Черты в PHP - какие-либо реальные примеры / лучшие практики? [закрыто]


148

Черты были одним из самых больших дополнений для PHP 5.4. Я знаю синтаксис и понимаю идею, лежащую в основе таких черт, как повторное использование горизонтального кода для таких общих вещей, как ведение журнала, безопасность, кэширование и т. Д.

Тем не менее, я до сих пор не знаю, как использовать черты в моих проектах.

Есть ли проекты с открытым исходным кодом, которые уже используют черты? Любые хорошие статьи / материалы для чтения о том, как структурировать архитектуры с использованием черт?


8
Вот мое мнение: пост в блоге на эту тему, который я написал по этой теме. TL; DR: По сути, я боюсь, что, хотя они мощные и могут быть использованы во благо, большинство видов использования, которые мы увидим, будут полными анти-паттернами и вызовут гораздо больше боли, чем решают ...
— ircmaxell

1
Взгляните на стандартную библиотеку Scala, и вы найдете много полезных примеров черт.
— Дмитрий

Ответы:


89

Мое личное мнение таково, что на самом деле очень мало приложений для черт при написании чистого кода.

Вместо использования признаков для взлома кода в классе лучше передать зависимости через конструктор или через сеттеры:

class ClassName {
    protected $logger;

    public function __construct(LoggerInterface $logger) {
        $this->logger = $logger;
    }
    // or
    public function setLogger(LoggerInterface $logger) {
        $this->logger = $logger;
    }
}

Основная причина, по которой я нахожу это лучше, чем использование черт, заключается в том, что ваш код становится намного более гибким, устраняя жесткую связь с чертой. Например, вы можете просто передать другой класс логгера сейчас. Это делает ваш код многоразовым и тестируемым.


4
Используя черты, вы также можете использовать другой класс логгера, верно? Просто отредактируйте черту, и все классы, которые используют черту, будут обновлены. Поправь меня, если я не прав
— rickchristie

14
@rickchristie Конечно, вы могли бы сделать это. Но вам нужно будет отредактировать исходный код черты. Таким образом, вы должны изменить его для каждого класса, использующего его, а не только для конкретного, для которого вы хотите использовать другой регистратор. А что, если вы хотите использовать один и тот же класс, но с двумя разными регистраторами? Или, если вы хотите пройти макет во время тестирования? Вы не можете, если вы используете черты, вы можете, если вы используете внедрение зависимостей.
— NikiC

2
Я понимаю вашу точку зрения, я также размышляю, стоят ли эти черты этого или нет. Я имею в виду, что в современных средах, таких как Symfony 2, вы повсеместно внедряете зависимости, которые в большинстве случаев кажутся превосходящими по своим чертам. На данный момент я вижу черты не намного больше, чем «копирование и вставка с помощью компилятора». ;)
— Макс

11
На данный момент я вижу черты не намного больше, чем «копирование и вставка с помощью компилятора». ;) : @Max: Это именно то, для чего были разработаны черты, так что это совершенно правильно. Это делает его более «обслуживаемым», поскольку существует только одно определение, но в основном это просто c & p ...
— ircmaxell

29
NikiC's упускает суть: использование черты не мешает использовать Dependency Injection. В этом случае признак просто позволил бы каждому классу, который реализует ведение журнала, не должен дублировать метод setLogger () и создание свойства $ logger. Черта обеспечит их. setLogger () напечатает подсказку на LoggerInterface, как в примере, так что любой тип регистратора может быть передан. Эта идея похожа на ответ Гордона ниже (только похоже, что он намекает на тип в суперклассе Logger, а не на интерфейс Logger ).
— Итан

205

Я полагаю, что в течение некоторого времени нужно искать языки, которые имеют черты, чтобы выучить принятые хорошие / лучшие практики. Мое текущее мнение о Trait состоит в том, что вы должны использовать их только для кода, который вам придется дублировать в других классах, которые имеют те же функции.

Пример для черты Logger:

interface Logger
{
    public function log($message, $level);    
}

class DemoLogger implements Logger
{
    public function log($message, $level)
    {
        echo "Logged message: $message with level $level", PHP_EOL; 
    }
}

trait Loggable // implements Logger
{
    protected $logger;
    public function setLogger(Logger $logger)
    {
        $this->logger = $logger;
    }
    public function log($message, $level)
    {
        $this->logger->log($message, $level);
    }
}

class Foo implements Logger
{
    use Loggable;
}

И тогда вы делаете ( демо )

$foo = new Foo;
$foo->setLogger(new DemoLogger);
$foo->log('It works', 1);

Я предполагаю, что при использовании черт важно учитывать, что они на самом деле являются просто частями кода, которые копируются в класс. Это может легко привести к конфликтам, например, когда вы пытаетесь изменить видимость методов, например

trait T {
    protected function foo() {}
}
class A { 
    public function foo() {}
}
class B extends A
{
    use T;
}

Вышеуказанное приведет к ошибке ( демо ). Аналогично, любые методы, объявленные в признаке, которые также уже объявлены в классе using, не будут скопированы в класс, например

trait T {
    public function foo() {
    return 1;
}
}
class A { 
    use T;
    public function foo() {
    return 2;
}
}

$a = new A;
echo $a->foo();

напечатает 2 ( демо ). Это те вещи, которые вы захотите избежать, поскольку они затрудняют поиск ошибок. Вы также захотите избегать помещения вещей в свойства, которые оперируют свойствами или методами класса, который его использует, например

class A
{
    use T;
    protected $prop = 1;
    protected function getProp() {
        return $this->prop;
    }
}

trait T
{
    public function foo()
    {
        return $this->getProp();
    }
}

$a = new A;
echo $a->foo();

работает ( демо ), но теперь черта тесно связана с А, и вся идея горизонтального повторного использования теряется.

Когда вы будете следовать принципу разделения интерфейсов, у вас будет много небольших классов и интерфейсов. Это делает Traits идеальным кандидатом для упомянутых вами вещей, например сквозных задач , но не для создания объектов (в структурном смысле). В приведенном выше примере с Logger эта черта полностью изолирована. У него нет зависимости от конкретных классов.

Мы могли бы использовать агрегацию / композицию (как показано в другом месте на этой странице) для достижения того же результирующего класса, но недостатком использования агрегации / композиции является то, что нам придется вручную добавлять методы прокси / делегатора к каждому классу, тогда это должно быть в состоянии войти. Черты решают эту проблему, позволяя мне хранить шаблон в одном месте и выборочно применять его там, где это необходимо.

Примечание: учитывая, что черты являются новой концепцией в PHP, все высказанные выше мнения могут быть изменены. У меня еще не было времени самому оценить концепцию. Но я надеюсь, что это достаточно хорошо, чтобы дать вам о чем подумать.


41
Это интересный вариант использования: используйте интерфейс, который определяет контракт, используйте черту, чтобы выполнить этот контракт. Хороший.
— Макс

13
Мне нравятся настоящие программисты, которые предлагают реальные рабочие примеры с кратким описанием для каждого. Спасибо
— Артур Кушман

1
Что если кто-то использует абстрактный класс вместо этого? Заменив интерфейс и черту, можно создать абстрактный класс. Также, если интерфейс так необходим для приложения, абстрактный класс может также реализовать интерфейс и определить методы, как это сделала trait. Так вы можете объяснить, почему нам все еще нужны черты?
— Суманчалки

12
@sumanchalki Абстрактный класс следует правилам наследования. Что если вам нужен класс, который реализует Loggable и Cacheable? Вам нужен класс для расширения AbstractLogger, который затем должен расширять AbstractCache. Но это означает, что все Loggables являются кэшами. Это соединение, которое вы не хотите. Это ограничивает повторное использование и портит ваш график наследования.
— Гордон

1
Я думаю, что демонстрационные ссылки мертвы
— Pmpr

19

:) Я не люблю теоретизировать и спорить о том, что нужно делать с чем-то. В этом случае черты. Я покажу вам, какие черты полезны для меня, и вы можете либо поучиться на них, либо проигнорировать их.

Черты - они отлично подходят для применения стратегий . Короче говоря, шаблоны разработки стратегии полезны, когда вы хотите, чтобы одни и те же данные обрабатывались (фильтровались, сортировались и т. Д.) По-разному.

Например, у вас есть список продуктов, которые вы хотите отфильтровать на основе некоторых критериев (бренды, характеристики, что угодно) или отсортировать по-разному (цена, ярлык, что угодно). Вы можете создать черту сортировки, которая содержит различные функции для разных типов сортировки (числовая, строковая, дата и т. Д.). Затем вы можете использовать эту черту не только в своем классе продукта (как показано в примере), но также и в других классах, которые нуждаются в аналогичных стратегиях (для применения числовой сортировки к некоторым данным и т. Д.).

Попытайся:

<?php
trait SortStrategy {
    private $sort_field = null;
    private function string_asc($item1, $item2) {
        return strnatcmp($item1[$this->sort_field], $item2[$this->sort_field]);
    }
    private function string_desc($item1, $item2) {
        return strnatcmp($item2[$this->sort_field], $item1[$this->sort_field]);
    }
    private function num_asc($item1, $item2) {
        if ($item1[$this->sort_field] == $item2[$this->sort_field]) return 0;
        return ($item1[$this->sort_field] < $item2[$this->sort_field] ? -1 : 1 );
    }
    private function num_desc($item1, $item2) {
        if ($item1[$this->sort_field] == $item2[$this->sort_field]) return 0;
        return ($item1[$this->sort_field] > $item2[$this->sort_field] ? -1 : 1 );
    }
    private function date_asc($item1, $item2) {
        $date1 = intval(str_replace('-', '', $item1[$this->sort_field]));
        $date2 = intval(str_replace('-', '', $item2[$this->sort_field]));
        if ($date1 == $date2) return 0;
        return ($date1 < $date2 ? -1 : 1 );
    }
    private function date_desc($item1, $item2) {
        $date1 = intval(str_replace('-', '', $item1[$this->sort_field]));
        $date2 = intval(str_replace('-', '', $item2[$this->sort_field]));
        if ($date1 == $date2) return 0;
        return ($date1 > $date2 ? -1 : 1 );
    }
}

class Product {
    public $data = array();

    use SortStrategy;

    public function get() {
        // do something to get the data, for this ex. I just included an array
        $this->data = array(
            101222 => array('label' => 'Awesome product', 'price' => 10.50, 'date_added' => '2012-02-01'),
            101232 => array('label' => 'Not so awesome product', 'price' => 5.20, 'date_added' => '2012-03-20'),
            101241 => array('label' => 'Pretty neat product', 'price' => 9.65, 'date_added' => '2012-04-15'),
            101256 => array('label' => 'Freakishly cool product', 'price' => 12.55, 'date_added' => '2012-01-11'),
            101219 => array('label' => 'Meh product', 'price' => 3.69, 'date_added' => '2012-06-11'),
        );
    }

    public function sort_by($by = 'price', $type = 'asc') {
        if (!preg_match('/^(asc|desc)$/', $type)) $type = 'asc';
        switch ($by) {
            case 'name':
                $this->sort_field = 'label';
                uasort($this->data, array('Product', 'string_'.$type));
            break;
            case 'date':
                $this->sort_field = 'date_added';
                uasort($this->data, array('Product', 'date_'.$type));
            break;
            default:
                $this->sort_field = 'price';
                uasort($this->data, array('Product', 'num_'.$type));
        }
    }
}

$product = new Product();
$product->get();
$product->sort_by('name');
echo '<pre>'.print_r($product->data, true).'</pre>';
?>

В заключение я думаю о таких чертах, как аксессуары (которые я могу использовать для изменения моих данных). Подобные методы и свойства, которые могут быть вырезаны из моих классов и помещены в одно место, для простоты обслуживания, более короткого и чистого кода.


1
Несмотря на то, что это поддерживает общедоступный интерфейс чистым, внутренний может стать очень сложным, особенно если вы распространите это на другие вещи, например, на цвета. Я думаю, что простые функции или статические методы лучше здесь.
— Себастьян Мах

Мне нравится термин strategies.
— Ранни Оллит

4

Я рад за черты, потому что они решают общую проблему при разработке расширений для платформы электронной коммерции Magento. Проблема возникает, когда расширения добавляют функциональность к базовому классу (например, модели User), расширяя его. Это делается путем указания автозагрузчику Zend (через файл конфигурации XML) использовать модель User из расширения, и эта новая модель расширяет базовую модель. ( пример ) Но что, если два расширения перекрывают одну и ту же модель? Вы получаете «состояние гонки», и загружается только одно.

В настоящее время решение состоит в том, чтобы отредактировать расширения таким образом, чтобы один расширял класс переопределения модели другого в цепочке, а затем настраивал конфигурацию расширения для загрузки их в правильном порядке, чтобы цепочка наследования работала.

Эта система часто вызывает ошибки, и при установке новых расширений необходимо проверять наличие конфликтов и редактировать расширения. Это боль и нарушает процесс обновления.

Я думаю, что использование Черт было бы хорошим способом выполнить то же самое без этой раздражающей модели, переопределяющей «состояние гонки». Конечно, могут быть конфликты, если несколько Traits реализуют методы с одинаковыми именами, но я думаю, что нечто вроде простого соглашения о пространстве имен может решить эту проблему по большей части.

TL; DR Я думаю, что Traits могут быть полезны для создания расширений / модулей / плагинов для больших программных пакетов PHP, таких как Magento.


0

Вы могли бы иметь черту для объекта только для чтения, как это:

  trait ReadOnly{  
      protected $readonly = false;

      public function setReadonly($value){ $this->readonly = (bool)$value; }
      public function getReadonly($value){ return $this->readonly; }
  }

Вы можете определить, используется ли эта черта, и определить, стоит ли записывать этот объект в базу данных, файл и т. Д.


Таким образом, класс, который будет useиметь эту черту, будет называться if($this -> getReadonly($value)); но это приведет к ошибке, если вы не сделали useэту черту. Поэтому этот пример ошибочен.
— Luceos

Что ж, сначала нужно проверить, используется ли эта черта. Если признак ReadOnly определен для объекта, вы можете проверить, доступен ли он только для чтения или нет.
— Нико

Я сделал общее доказательство концепции такой черты в gist.github.com/gooh/4960073
— Гордон

3
Вы должны объявить интерфейс для ReadOnly для этой цели
— Майкл Цанг
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.