Черты против интерфейсов

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

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

Может ли кто-нибудь поделиться своим мнением / взглядом на это?

Интерфейс определяет набор методов, которые должен реализовать класс реализации.

Когда используется черта, d также реализуются реализации методов, чего не происходит в Interface .

Это самая большая разница.

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

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

Объявление о государственной службе:

Я хочу заявить для записи, что, я считаю, черты почти всегда являются запахами кода, и их следует избегать в пользу композиции. Мое мнение, что единичное наследование часто злоупотребляют до такой степени, что оно является анти-шаблоном, а множественное наследование только объединяет эту проблему. В большинстве случаев вам будет гораздо лучше подавать предпочтение композиции над наследованием (будь то одно или несколько). Если вы все еще интересуетесь чертами и их отношением к интерфейсам, читайте далее …


Начнем с того, что:

Объектно-ориентированное программирование (ООП) может быть сложной парадигмой. Просто потому, что вы используете классы, это не значит, что ваш код является Object-Oriented (OO).

Чтобы написать OO-код, вам нужно понять, что ООП действительно о возможностях ваших объектов. Вы должны думать о классах с точки зрения того, что они могут сделать, а не того, что они на самом деле делают . Это резко контрастирует с традиционным процедурным программированием, в котором основное внимание уделяется созданию кода «что-то».

Если ООП-код посвящен планированию и дизайну, интерфейс – это проект, а объект – полностью построенный дом. Между тем, черты – это просто способ помочь построить дом, построенный по проекту (интерфейс).

Интерфейсы

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

Интерфейс – это контракт между программистом и его / ее кодом. Интерфейс говорит: «Пока вы играете по моим правилам, вы можете реализовать меня, как вам нравится, и я обещаю, что не нарушу ваш другой код».

В качестве примера рассмотрим реальный сценарий (без автомобилей или виджетов):

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

Вы начинаете с написания класса для ответов на запросы кеша с помощью 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 из обсуждения интерфейса используют тот же метод, чтобы определить, является ли запись в кэше устаревшей и ее необходимо удалить (очевидно, это не так в реальной жизни, но идти с ней). Вы можете написать черту и разрешить использовать оба класса для общего требования к интерфейсу.

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

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 данных IBaseInterface[] bases может содержать экземпляры MyClass . Нет такой полиморфной конструкции, доступной с trait потому что 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. Свойство myTrait предоставляет функцию без параметров, называемую SomeMethod() , которая имеет приоритет над BaseClass
  4. class MyClass предоставляет свою собственную версию SomeMethod()которая имеет приоритет над trait -version.

Вывод

  1. Interface не может обеспечить стандартную реализацию тела метода, в то время как trait может.
  2. Interface представляет собой полиморфную , унаследованную конструкцию, а trait – нет.
  3. Несколько Interface s могут использоваться в одном классе, и поэтому могут иметь несколько trait .

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

Например:

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

Вы можете использовать этот метод «error» в любом классе, который использует этот признак.

 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 . Это не относится к чертам.

Это совершенно по-другому!

Часто используемая метафора для описания признаков – это черты, которые являются интерфейсами с реализацией.

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

Для начала оператор instanceof не будет работать с чертами (т. Е. Черта не является реальным объектом), поэтому вы не можете нам этого узнать, имеет ли класс определенный признак (или чтобы увидеть, имеют ли два других не связанных между собой класса черта). Это то, что они подразумевают под этим, являясь конструкцией для повторного использования горизонтального кода.

Теперь в PHP есть функции, которые позволят вам получить список всех признаков, которые использует класс, но наследование trait означает, что вам нужно будет делать рекурсивные проверки, чтобы надежно проверить, имеет ли класс в какой-то момент определенный признак (есть пример код на страницах PHP doco). Но да, это, конечно, не так просто и чисто, как 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 Keyed или нет, вы знаете, что получите согласованный набор методов и т. Д., И весь код находится в одном месте по всем классам, использующим KeyedTrait.

Черты просто для повторного использования кода .

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

Для справки – http://www.php.net/manual/en/language.oop5.traits.php

Вы можете рассматривать Trait как автоматическую «копию-вставку» кода, в основном.

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

Тем не менее, черты более гибкие из-за отсутствия таких ограничений, как наследование.

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

Для читающих французский человек, которые могут его получить, в журнале GNU / Linux HS 54 есть статья по этому вопросу.

Интерфейс – это контракт, в котором говорится, что «этот объект способен делать эту вещь», тогда как «Тит» дает объекту возможность делать что-то.

Значок – это, по сути, способ «скопировать и вставить» код между классами.

попробуйте прочитать эту статью

Если вы знаете английский и знаете, что означает, это именно то, что говорит это имя. Это класс без классов и свойств, которые вы добавляете к существующим классам, набрав use .

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

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

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

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

Добавление возможностей публикации публикации / подписки в класс может быть распространенным сценарием в некоторых базовых кодах. Существует 3 общих решения:

  1. Определите базовый класс с событием pub / sub code, а затем классы, которые хотят предлагать события, могут расширить его, чтобы получить возможности.
  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 точно такой же, как внутри класса triggerEvent() за исключением того, что признак объявляет метод triggerEvent() как защищенный. Итак, единственное отличие, на которое вам нужно обратить внимание, – это реализация класса Auction .

И разница большая. При использовании композиции мы получаем отличное решение, позволяющее нам повторно использовать наш EventEmitter на столько классов, сколько нам нравится. Но главным недостатком является то, что у нас есть много кода шаблона, который нам нужно написать и поддерживать, потому что для каждого метода, определенного в интерфейсе Observable , нам нужно его реализовать и написать расточный шаблон шаблона, который просто перенаправляет аргументы на соответствующий метод в нашем EventEmitter объекте EventEmitter . Использование признака в этом примере позволяет нам избежать этого , помогая нам уменьшить код шаблона и улучшить ремонтопригодность .

Тем не менее, могут быть случаи, когда вы не хотите, чтобы ваш класс Auction реализовал полный интерфейс Observable – возможно, вы хотите только разоблачить 1 или 2 метода или, может быть, даже совсем нет, чтобы вы могли определять свои собственные сигнатуры методов. В таком случае вы, возможно, предпочтете метод композиции.

Но в большинстве сценариев эта особенность очень важна, особенно если интерфейс имеет множество методов, что заставляет писать много шаблонов.

* На самом деле вы могли бы обойтись и тем и другим – определить класс EventEmitter если вы когда-либо захотите использовать его композиционно, и также определить свойство EventEmitterTrait , используя реализацию класса EventEmitter внутри этого признака 🙂