Риси та інтерфейси


344

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

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


6
Інтерфейс не має коду в органах функцій. вони фактично не мають жодних функціональних органів.
hakre

2
Незважаючи на мою відповідь, яку я дуже схвалював, я хотів би, щоб у записі було зазначено, що я, як правило, антитрейт / міксин . Перевірте цю стенограму чату, щоб прочитати, як риси часто підривають тверді практики OOP .
rdlowrey

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

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

Відповіді:


239

Інтерфейс визначає набір методів, які повинен реалізувати клас реалізації.

Якщо ознака use"d", то реалізуються методи також, що не відбувається в Interface.

Це найбільша різниця.

З горизонтального повторного використання для PHP RFC :

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


2
@JREAM На практиці нічого. Насправді набагато більше.
Алек Ущелиця

79
За винятком того, що риси зовсім не є інтерфейсами. Інтерфейси - це специфікація, яку можна перевірити. Не можна перевірити ознаки, тому вони є лише реалізацією. Вони абсолютно протилежні інтерфейсам. Цей рядок у RFC просто неправильний ...
ircmaxell

195
Риси - це фактично мовна копія та вставка .
Шахід

10
Це не метафора. Це розбиття значення слова. Це як описувати коробку як поверхню з об’ємом.
cleong

6
Для розширення коментарів ircmaxell та Shadi: Ви можете перевірити, чи об’єкт реалізує інтерфейс (через instanceof), і ви можете переконатися, що аргумент методу реалізує інтерфейс через підказку типу в підписі методу. Ви не можете виконати відповідні перевірки ознак.
Брайан Д'Астоус

530

Оголошення про державну службу:

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


Почнемо з цього:

Об'єктно-орієнтоване програмування (ООП) може бути важкою парадигмою. Тільки тому, що ви використовуєте класи, не означає, що ваш код орієнтований на об'єкти (OO).

Щоб написати код OO, вам потрібно зрозуміти, що OOP дійсно стосується можливостей ваших об'єктів. Ви повинні думати про заняття з точки зору того, що вони можуть зробити, а не те, що вони насправді роблять . Це суворо на відміну від традиційного процедурного програмування, де акцент робиться на створенні трохи коду "зроби щось".

Якщо код OOP стосується планування та проектування, інтерфейс - це план, а об'єктом є повністю побудований будинок. Тим часом риси - це просто спосіб допомогти побудувати будинок, викладений планом (інтерфейсом).

Інтерфейси

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

Інтерфейс - це договір між програмістом та його кодом. В інтерфейсі написано: "Поки ти граєш за моїми правилами, ти можеш реалізовувати мене як завгодно, і я обіцяю, що я не порушу твій інший код".

Отже, як приклад, розгляньте сценарій у реальному світі (без автомобілів чи віджетів):

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

Ви починаєте з написання класу для кешування відповідей на запит за допомогою APC:

class ApcCacher
{
  public function fetch($key) {
    return apc_fetch($key);
  }
  public function store($key, $data) {
    return apc_store($key, $data);
  }
  public function delete($key) {
    return apc_delete($key);
  }
}

Потім у вашому об’єкті відповіді HTTP ви перевіряєте на наявність кешу, перш ніж виконати всю роботу, щоб створити фактичну відповідь:

class Controller
{
  protected $req;
  protected $resp;
  protected $cacher;

  public function __construct(Request $req, Response $resp, ApcCacher $cacher=NULL) {
    $this->req    = $req;
    $this->resp   = $resp;
    $this->cacher = $cacher;

    $this->buildResponse();
  }

  public function buildResponse() {
    if (NULL !== $this->cacher && $response = $this->cacher->fetch($this->req->uri()) {
      $this->resp = $response;
    } else {
      // Build the response manually
    }
  }

  public function getResponse() {
    return $this->resp;
  }
}

Цей підхід чудово працює. Але, можливо, через кілька тижнів ви вирішите, що хочете використовувати кешовану файлову систему замість APC. Тепер ви повинні змінити код свого контролера, оскільки ви запрограмували свій контролер працювати з функціональністю ApcCacherкласу, а не з інтерфейсом, що виражає можливості ApcCacherкласу. Скажімо, замість вищесказаного ви зробили Controllerклас, спираючись на CacherInterfaceбетон ApcCacherтак:

// Your controller's constructor using the interface as a dependency
public function __construct(Request $req, Response $resp, CacherInterface $cacher=NULL)

Для цього ви визначаєте свій інтерфейс так:

interface CacherInterface
{
  public function fetch($key);
  public function store($key, $data);
  public function delete($key);
}

У свою чергу, ви ApcCacherі ваш новий FileCacherклас реалізуєте, CacherInterfaceі ви програмуєте свій Controllerклас на використання можливостей, необхідних інтерфейсом.

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

Риси

З іншого боку, риси - це просто метод повторного використання коду. Інтерфейси не слід розглядати як взаємовиключну альтернативу ознак. Фактично, створення ознак, які відповідають можливостям, необхідним інтерфейсом, є ідеальним випадком використання .

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

Розглянемо таку реалізацію ознаки:

interface Person
{
    public function greet();
    public function eat($food);
}

trait EatingTrait
{
    public function eat($food)
    {
        $this->putInMouth($food);
    }

    private function putInMouth($food)
    {
        // Digest delicious food
    }
}

class NicePerson implements Person
{
    use EatingTrait;

    public function greet()
    {
        echo 'Good day, good sir!';
    }
}

class MeanPerson implements Person
{
    use EatingTrait;

    public function greet()
    {
        echo 'Your mother was a hamster!';
    }
}

Більш конкретний приклад: уявіть, що і ваш, FileCacherі ваш ApcCacherіз обговорення інтерфейсу використовують один і той же метод, щоб визначити, чи є запис кеша несвіжим і його слід видалити (очевидно, це не так у реальному житті, але йдіть з ним). Ви можете написати ознаку і дозволити обом класам використовувати її для загальної інтерфейсної вимоги.

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


69
Я дуже шукав швидку просту відповідь, подану вище, але мушу сказати, що ви дали чудову глибоку відповідь, яка допоможе зробити розрізнення зрозумілішим для інших, кудо.
datguywhowanders

35
"[C] ринкові характеристики, що відповідають можливостям, необхідним інтерфейсом у даному класі, є ідеальним випадком використання". Рівно: +1
Alec Gorge

5
Чи було б справедливо сказати, що риси PHP схожі на комбінації інших мов?
Ено

5
@igorpan Для всіх намірів і цілей , я б сказав , реалізація рисою РНР є такий же , як множинне спадкування. Варто відзначити, що якщо ознака в PHP задає статичні властивості, то кожен клас, що використовує ознаку, матиме власну копію статичної властивості. Що ще важливіше ... бачачи, як ця публікація зараз надзвичайно висока в СЕРП під час запитів ознак, я збираюся додати оголошення про публічну службу вгору сторінки. Ви повинні прочитати його.
rdlowrey

3
+1 для поглибленого пояснення. Я походжу з рубінового фону, де в міксинах використовується ЛОТ; просто щоб додати два мої центи, хороше правило, яке ми використовуємо, може бути переведене на php як "не застосовувати методи, що мутують $ this у рисах". Це запобігає цілій купі божевільних налагоджувальних сесій ... Міксин також НЕ повинен робити жодних припущень щодо класу, в який він буде змішаний (або вам слід зробити це дуже зрозумілим і зменшити залежності до простого мінімуму). У зв'язку з цим я вважаю вашу ідею рис, що реалізують інтерфейси, приємними.
m_x

67

По traitсуті, це PHP-реалізація mixin, і фактично є набором методів розширення, які можна додати до будь-якого класу шляхом додавання trait. Потім методи стають частиною реалізації цього класу, але без використання успадкування .

З посібника з PHP (моє наголос):

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

Приклад:

trait myTrait {
    function foo() { return "Foo!"; }
    function bar() { return "Bar!"; }
}

З визначеною вище ознакою я тепер можу зробити наступне:

class MyClass extends SomeBaseClass {
    use myTrait; // Inclusion of the trait myTrait
}

У цей момент, коли я створюю екземпляр класу MyClass, він має два методи, звані foo()і bar()- які походять myTrait. І - зауважте, що у traitвизначених методів вже є тіло методу - який Interfaceвизначений метод не може.

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

Кілька речей, які слід зазначити:

                      -----------------------------------------------
                      |   Interface   |  Base Class   |    Trait    |
                      ===============================================
> 1 per class         |      Yes      |       No      |     Yes     |
---------------------------------------------------------------------
Define Method Body    |      No       |       Yes     |     Yes     |
---------------------------------------------------------------------
Polymorphism          |      Yes      |       Yes     |     No      |
---------------------------------------------------------------------

Поліморфізм:

У попередньому прикладі, де MyClass поширюється SomeBaseClass , MyClass - це екземпляр SomeBaseClass. Іншими словами, масив, такий як SomeBaseClass[] basesможе містити екземпляри MyClass. Аналогічно, якщо MyClassрозширено IBaseInterface, масив IBaseInterface[] basesможе містити екземпляри MyClass. Немає такої поліморфної конструкції, яка доступна з trait- тому, що по traitсуті є просто кодом, який скопіюється для зручності програміста в кожен клас, який його використовує.

Пріоритетність:

Як описано в Посібнику:

Спадковий член з базового класу перекриває член, введений ознакою. Порядок пріоритетності полягає в тому, що члени з поточного класу переосмислюють методи Trait, які взамін переосмислюють успадковані методи.

Отже - врахуйте наступний сценарій:

class BaseClass {
    function SomeMethod() { /* Do stuff here */ }
}

interface IBase {
    function SomeMethod();
}

trait myTrait {
    function SomeMethod() { /* Do different stuff here */ }
}

class MyClass extends BaseClass implements IBase {
    use myTrait;

    function SomeMethod() { /* Do a third thing */ }
}

При створенні екземпляра MyClass, вище, відбувається наступне:

  1. Interface IBaseВимагає функцію без параметрів під назвою SomeMethod()повинні бути надані.
  2. Базовий клас BaseClassзабезпечує реалізацію цього методу - задоволення потреби.
  3. Також trait myTraitнадається функція без параметрів, що називається SomeMethod(), яка має перевагу над BaseClass-версією
  4. class MyClassНадає свою власну версію SomeMethod()- яка має пріоритет над trait-Версія.

Висновок

  1. Не Interfaceможе забезпечити реалізацію за замовчуванням тіла методу, тоді як traitможе.
  2. А Interfaceє поліморфною , успадкованою конструкцією - а а trait- ні.
  3. Кілька Interfaces можуть використовуватися в одному класі, і так само може бути декілька traits.

4
"Ознака схожа на концепцію C # абстрактного класу" Ні, абстрактний клас - абстрактний клас; ця концепція існує і в PHP, і в C #. Я б порівнював ознаку в PHP зі статичним класом, створеним методами розширення в C #, замість цього видалене обмеження на основі типу як риса може бути використане майже будь-яким типом, на відміну від методу розширення, який розширює лише один тип.
BoltClock

1
Дуже хороший коментар - і я з вами згоден. Якщо перечитати, це краща аналогія. Я вважаю, що все-таки краще все-таки вважати це як mixin- і, як я переглянув відкриття своєї відповіді, я оновив, щоб це відобразити. Дякуємо за коментар, @BoltClock!
Трой Алфорд

1
Я не думаю, що до методів розширення c # немає ніякого відношення. Методи розширення додаються до типу одного класу (з урахуванням ієрархії класів, звичайно), їх мета полягає в тому, щоб покращити тип з додатковою функціональністю, а не "ділити код" на декілька класів і безладно. Це не можна порівняти! Якщо щось потрібно повторно використовувати, це означає, що він повинен мати власний простір, як окремий клас, який би був пов'язаний з класами, які потребують загальної функціональності. Реалізація може змінюватися залежно від дизайну, але приблизно це все. Риси - це ще один спосіб зробити поганий код.
Софія

Клас може мати декілька інтерфейсів? Я не впевнений, чи я неправильно розумію ваш графік, але клас X реалізує Y, Z дійсний.
Ян Чабо

26

Я думаю traits, що корисно створити класи, які містять методи, які можна використовувати як методи декількох різних класів.

Наприклад:

trait ToolKit
{
    public $errors = array();

    public function error($msg)
    {
        $this->errors[] = $msg;
        return false;
    }
}

Цей метод "помилки" можна використовувати та використовувати в будь-якому класі, який використовує цю ознаку.

class Something
{
    use Toolkit;

    public function do_something($zipcode)
    {
        if (preg_match('/^[0-9]{5}$/', $zipcode) !== 1)
            return $this->error('Invalid zipcode.');

        // do something here
    }
}

Хоча з interfacesвами можна оголосити лише підпис методу, але не код його функцій. Також, щоб використовувати інтерфейс, потрібно дотримуватися ієрархію, використовуючи implements. Це не стосується рис.

Зовсім інше!


Я думаю, що це поганий приклад риси. to_integerшвидше було б включено в IntegerCastінтерфейс, оскільки принципово не схожий спосіб (інтелектуально) класувати класи на ціле число.
Матвій

5
Забудьте "to_integer" - це просто ілюстрація. Приклад. А «Привіт, світ». "Example.com".
Дж. Бруні

2
Яку перевагу забезпечує ця ознака інструментарію тим, що окремий клас корисності не міг? Замість того, щоб use Toolkitви могли мати $this->toolkit = new Toolkit();чи я пропускаю якусь користь від самої риси?
Ентоні

@Anthony десь у Somethingконтейнері, який ви робитеif(!$something->do_something('foo')) var_dump($something->errors);
TheRealChx101

20

Для початківців вище відповідь може бути важкою. Це найпростіший спосіб зрозуміти це:

Риси

trait SayWorld {
    public function sayHello() {
        echo 'World!';
    }
}

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

class MyClass{
  use SayWorld;

}

$o = new MyClass();
$o->sayHello();

Класна справа!

Не тільки функції, якими ви можете користуватися чим завгодно в ознаці (функція, змінні, const ..). також ви можете використовувати кілька ознак:use SayWorld,AnotherTraits;

Інтерфейс

  interface SayWorld {
     public function sayHello();
  }

  class MyClass implements SayWorld { 
     public function sayHello() {
        echo 'World!';
     }
}

так це інтерфейс, відмінний від ознак: Вам потрібно заново створити все в інтерфейсі в реалізованому класі. інтерфейс не має реалізації. і інтерфейс може мати лише функції та const, він не може мати змінних.

Я сподіваюся, що це допомагає!


5

Часто використовується метафора для опису ознак: Риси є інтерфейсами реалізації.

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

Для початку instanceofоператор не буде працювати з ознаками (тобто, ознака не є реальним об'єктом), тому ви не можете нам це перевірити, чи є у класу певна ознака (або щоб побачити, чи не мають між собою два класу, що не пов'язані між собою, між собою) ). Це те, що вони розуміють, будучи конструкцією для горизонтального використання коду.

Зараз у PHP є функції, які дозволять вам отримати список усіх ознак, якими користується клас, але успадкування ознак означає, що вам потрібно буде робити рекурсивні перевірки, щоб надійно перевірити, чи є клас у певний момент певною ознакою (є приклад код на сторінках doco PHP). Але так, це, звичайно, не так просто і чисто, як instanceof, і IMHO - це особливість, яка зробить PHP кращим.

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

Я виявив, що риси та інтерфейси дійсно добре використовувати рука об руку, щоб створити множинне псевдонадання. Наприклад:

class SlidingDoor extends Door implements IKeyed  
{  
    use KeyedTrait;  
    [...] // Generally not a lot else goes here since it's all in the trait  
}

Це означає, що ви можете використовувати instanceof, щоб визначити, чи певний об’єкт Door є ключевим чи ні, ви знаєте, що ви отримаєте послідовний набір методів тощо, і весь код знаходиться в одному місці для всіх класів, які використовують KeyedTrait.


Остання частина цієї відповіді - це, звичайно, те, що @rdlowrey більш докладно розповідає в останніх трьох абзацах у розділі "Риси" у своєму посту; Я просто відчував, що дійсно простий фрагмент коду скелета допоможе проілюструвати його.
Джон Клоске

Я думаю, що найкращий спосіб використання ознак - це використання інтерфейсів, де можна. І якщо є випадок, коли декілька підкласів, що реалізують один і той же тип коду для цього інтерфейсу, і ви не можете перемістити цей код до їх (абстрактного) суперкласу -> реалізувати його з рисами
player-one

4

Риси просто для повторного використання коду .

Інтерфейс просто забезпечує підпис функцій, які мають бути визначені в класі, де його можна використовувати залежно від розсуду програміста . Таким чином даємо нам прототип для групи занять .

Для довідок- http://www.php.net/manual/en/language.oop5.traits.php


3

Ви можете розглядати ознаку як автоматизовану "копію-вставку" коду, в основному.

Використовувати риси небезпечно, оскільки немає сенсу знати, що вона робить перед виконанням.

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

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

Для людей, які читають французів, які можуть її отримати, журнал GNU / Linux HS 54 має статтю на цю тему.


Ще не зрозуміти, чим риси відрізняються від інтерфейсів із реалізацією за замовчуванням
denis631

@ denis631 Ви можете бачити риси як фрагменти коду, а інтерфейси як договори підпису. Якщо це може допомогти, ви можете розглядати це як неформальний біт класу, який може містити що завгодно. Дайте мені знати, чи допоможе це.
Бендж

Я бачу, що риси PHP можна розглядати як макроси, які потім розгортаються під час компіляції / просто зшиваючи фрагмент коду за допомогою цього ключа. Однак риси іржі виявляються різними (або я помиляюся). Але оскільки у них обох є ознака слова, я вважаю, що вони однакові, тобто те саме поняття. Риси іржі посилання: doc.rust-lang.org/rust-by-example/ Portrait.html
denis631

2

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

В основному, ви можете порівняти його з однією змінною. Функції закриття можуть використовувати useці змінні за межами області, і таким чином вони мають значення всередині. Вони потужні і можуть бути використані у всьому. Те саме відбувається і з рисами, якщо вони використовуються.


2

Інші відповіді чудово спрацювали з поясненням відмінностей між інтерфейсами та ознаками. Я зупинюсь на корисному прикладі реального світу, зокрема, який демонструє, що ознаки можуть використовувати змінні екземпляри - дозволяючи додати поведінку до класу з мінімальним кодовим кодом.

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

Додавання можливостей публікації / підписки подій до класу може бути звичайним сценарієм у деяких базах коду. Є 3 поширених рішення:

  1. Визначте базовий клас з паб / підкодом події, а потім класи, які хочуть запропонувати події, можуть розширити його, щоб отримати можливості.
  2. Визначте клас з паб / підкодом події, а потім інші класи, які хочуть запропонувати події, можуть використовувати його за допомогою композиції, визначаючи власні методи обертання складеного об'єкта, наближаючи виклики методу до нього.
  3. Визначте ознаку з паб / підкодом події, а потім інші класи, які хочуть запропонувати події, можуть useознаку, яка також імпортує її, отримати можливості.

Наскільки добре працює кожен?

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

№2 і №3 добре працюють. Я покажу приклад, який висвітлює деякі відмінності.

По-перше, деякий код, який буде однаковим між обома прикладами:

Інтерфейс

interface Observable {
    function addEventListener($eventName, callable $listener);
    function removeEventListener($eventName, callable $listener);
    function removeAllEventListeners($eventName);
}

І деякий код для демонстрації використання:

$auction = new Auction();

// Add a listener, so we know when we get a bid.
$auction->addEventListener('bid', function($bidderName, $bidAmount){
    echo "Got a bid of $bidAmount from $bidderName\n";
});

// Mock some bids.
foreach (['Moe', 'Curly', 'Larry'] as $name) {
    $auction->addBid($name, rand());
}

Гаразд, тепер покажемо, як Auctionбуде відрізнятися реалізація класу при використанні ознак.

По-перше, ось як виглядатиме №2 (використовуючи композицію):

class EventEmitter {
    private $eventListenersByName = [];

    function addEventListener($eventName, callable $listener) {
        $this->eventListenersByName[$eventName][] = $listener;
    }

    function removeEventListener($eventName, callable $listener) {
        $this->eventListenersByName[$eventName] = array_filter($this->eventListenersByName[$eventName], function($existingListener) use ($listener) {
            return $existingListener === $listener;
        });
    }

    function removeAllEventListeners($eventName) {
        $this->eventListenersByName[$eventName] = [];
    }

    function triggerEvent($eventName, array $eventArgs) {
        foreach ($this->eventListenersByName[$eventName] as $listener) {
            call_user_func_array($listener, $eventArgs);
        }
    }
}

class Auction implements Observable {
    private $eventEmitter;

    public function __construct() {
        $this->eventEmitter = new EventEmitter();
    }

    function addBid($bidderName, $bidAmount) {
        $this->eventEmitter->triggerEvent('bid', [$bidderName, $bidAmount]);
    }

    function addEventListener($eventName, callable $listener) {
        $this->eventEmitter->addEventListener($eventName, $listener);
    }

    function removeEventListener($eventName, callable $listener) {
        $this->eventEmitter->removeEventListener($eventName, $listener);
    }

    function removeAllEventListeners($eventName) {
        $this->eventEmitter->removeAllEventListeners($eventName);
    }
}

Ось як виглядатиме №3 (риси):

trait EventEmitterTrait {
    private $eventListenersByName = [];

    function addEventListener($eventName, callable $listener) {
        $this->eventListenersByName[$eventName][] = $listener;
    }

    function removeEventListener($eventName, callable $listener) {
        $this->eventListenersByName[$eventName] = array_filter($this->eventListenersByName[$eventName], function($existingListener) use ($listener) {
            return $existingListener === $listener;
        });
    }

    function removeAllEventListeners($eventName) {
        $this->eventListenersByName[$eventName] = [];
    }

    protected function triggerEvent($eventName, array $eventArgs) {
        foreach ($this->eventListenersByName[$eventName] as $listener) {
            call_user_func_array($listener, $eventArgs);
        }
    }
}

class Auction implements Observable {
    use EventEmitterTrait;

    function addBid($bidderName, $bidAmount) {
        $this->triggerEvent('bid', [$bidderName, $bidAmount]);
    }
}

Зауважте, що код всередині EventEmitterTraitточно такий же, як і всередині EventEmitterкласу, за винятком ознаки, декларує triggerEvent()метод захищеним. Отже, єдина різниця, на яку потрібно звернути увагу, - це реалізація Auctionкласу .

І різниця велика. Використовуючи композицію, ми отримуємо чудове рішення, що дозволяє повторно використовувати наш EventEmitterстільки класів, скільки нам подобається. Але головним недоліком є ​​те, що у нас є багато кодового шаблону, який нам потрібно записати та підтримувати, оскільки для кожного методу, визначеного в Observableінтерфейсі, нам потрібно його реалізувати і записати нудний код коробки, який просто пересилає аргументи на відповідний метод у наш EventEmitterоб'єкт склав . Використання ознаки в цьому прикладі дозволяє нам уникнути цього , допомагаючи нам зменшити код котла та покращити ремонтопридатність .

Однак можуть бути випадки, коли ви не хочете, щоб ваш Auctionклас реалізував повний Observableінтерфейс - можливо, ви хочете розкрити лише 1 або 2 способи, а може навіть і зовсім жоден, щоб ви могли визначити власні підписи методу. У такому випадку ви все ще можете віддати перевагу методу композиції.

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

* Можна насправді зробити і те й інше - визначте EventEmitterклас у випадку, якщо ви хочете використовувати його композиційно, і також визначте його EventEmitterTrait, використовуючи реалізацію EventEmitterкласу всередині ознаки :)


1

Ця ознака є такою ж, як і клас, який ми можемо використовувати для множинних цілей успадкування, а також повторне використання коду.

Ми можемо використовувати ознаки всередині класу, а також можемо використовувати декілька ознак в одному класі з «ключовим словом».

Інтерфейс використовується для повторного використання коду так само, як ознака

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

http://php.net/manual/en/language.oop5.traits.php http://php.net/manual/en/language.oop5.interfaces.php



0

Основна відмінність полягає в тому, що за допомогою інтерфейсів ви повинні визначити фактичну реалізацію кожного методу в кожному класі, який реалізує згаданий інтерфейс, тому у вас може бути багато класів, що реалізують один і той же інтерфейс, але з різною поведінкою, тоді як риси - це лише шматки коду, введеного в клас; Ще одна важлива відмінність полягає в тому, що методи ознаки можуть бути лише методами класу або статичними методами, на відміну від методів інтерфейсу, які також можуть (і зазвичай є) методами екземплярів.

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