Черты в 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.