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

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

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

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

Ответ 1

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

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

class ClassName {
    protected $logger;

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

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

Ответ 2

Я предполагаю, что нужно было бы изучить языки, на которых есть "Значения" в течение некоторого времени, чтобы изучить принятые Хорошие/Лучшие практики. Мое настоящее мнение о 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;
}

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

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();

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

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

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

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

Ответ 3

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

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

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

Попробуйте:

<?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>';
?>

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

Ответ 4

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

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

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

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

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

Ответ 5

У вас может быть черта для объекта только для чтения:

  trait ReadOnly{  
      protected $readonly = false;

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

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