Риси PHP - будь-які реальні приклади / кращі практики? [зачинено]


148

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

Однак я досі не знаю, як би я використовував риси у своїх проектах.

Чи є проекти з відкритим кодом, які вже використовують риси? Будь-які хороші статті / матеріал для читання про те, як структурувати архітектури, використовуючи риси?


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

1
Погляньте на стандартну бібліотеку Scala, і ви дізнаєтесь багато корисних прикладів рис.
dmitry

Відповіді:


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 немає пункту: використання ознаки не перешкоджає використанню залежної ін'єкції. У цьому випадку ознака просто дозволить кожному класу, який реалізує журнал, не потрібно дублювати метод setLogger () та створення властивості $ logger. Ця риса забезпечувала б їх. setLogger () буде вводити натяк на LoggerInterface, як у прикладі, так що будь-який тип реєстратора може бути переданий. Ця ідея схожа на відповідь Гордона нижче (тільки це виглядає так, що він натякає на суперклас Logger, а не інтерфейс Logger ).
Етан

205

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

Приклад ознаки журналу:

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;
}

Вищезазначене призведе до помилки ( демонстрації ). Аналогічно, будь-які способи, оголошені в ознаці, які також вже оголошені в використовуваному класі, не будуть скопійовані в клас, наприклад

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

працює ( демонстрація ), але тепер риса тісно пов'язана з A і вся ідея горизонтального повторного використання втрачається.

Коли ви будете слідувати інтерфейс сегрегація Принцип ви будете мати багато дрібних класів і інтерфейсів. Це робить Риси ідеальним кандидатом для речей, про які ви згадали, наприклад, перехресного вирішення питань , але не для створення об'єктів (у структурному сенсі). У нашому прикладі Logger вище, ознака повністю ізольована. Він не має залежності від конкретних класів.

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

Примітка: враховуючи, що ознаки є новим поняттям в PHP, вся думка, висловлена ​​вище, може змінюватися. Я ще не мав багато часу, щоб сам оцінити цю концепцію. Але я сподіваюся, що це досить добре, щоб дати вам щось подумати.


41
Це цікавий випадок використання: використовуйте інтерфейс, який визначає контракт, використовуйте ознаки для того, щоб задовольнити цей договір. Хороший.
Макс

13
Мені подобається такий справжній програміст, який пропонує справжні робочі приклади з коротким описом для кожного. Thx
Артур Кушман

1
Що робити, якщо хтось замість цього використовує абстрактний клас? Замінивши інтерфейс та ознаку, можна створити абстрактний клас. Крім того, якщо інтерфейс так необхідний для програми, абстрактний клас також може реалізувати інтерфейс та визначити такі методи, як trait did. Тож чи можете ви пояснити, чому нам ще потрібні риси?
sumanchalki

12
@sumanchalki Анотаційний клас дотримується правил успадкування. Що робити, якщо вам знадобився клас, який реалізує протокол реєстрації та кешування? Вам знадобиться клас для розширення AbstractLogger, який потім повинен розширити AbstractCache. Але це означає, що всі Журнали є кешами. Це муфта, яку ви не хочете. Це обмежує повторне використання та змінює ваш графік спадкування.
Гордон

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. Проблема виникає, коли розширення додають функціональність до основного класу (наприклад, модель користувача), розширюючи його. Це робиться, вказуючи на автозавантажувач Zend (через файл конфігурації XML), щоб використовувати модель користувача з розширення, і щоб ця нова модель розширила основну модель. ( приклад ) Але що робити, якщо два розширення замінюють одну і ту ж модель? Ви отримуєте "стан перегонів", і завантажується лише одна.

Рішення зараз полягає в редагуванні розширень, щоб один розширював клас переозброєння моделі в ланцюзі, а потім встановлював конфігурацію розширення для завантаження їх у правильному порядку, щоб ланцюг спадкування працював.

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

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

TL; DR Я думаю, що риси можуть бути корисні для створення розширень / модулів / плагінів для великих програмних пакетів 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
Gordon

3
Вам слід оголосити інтерфейс для ReadOnly для цієї мети
Майкл Цан
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.