Черты против интерфейсов - PullRequest
313 голосов
/ 09 февраля 2012

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

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

Ответы [ 13 ]

503 голосов
/ 09 февраля 2012

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

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


Давайте начнем с того, что скажем:

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

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

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

Интерфейсы

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

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

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

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

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

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

224 голосов
/ 09 февраля 2012

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

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

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

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

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

64 голосов
/ 02 декабря 2012

A trait, по сути, является реализацией mixin в PHP и фактически представляет собой набор методов расширения, которые можно добавить в любой класс путем добавления trait.Методы затем становятся частью реализации этого класса, но без использования наследования .

Из PHP Manual (выделено мной):

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

Заключение

  1. Interface не может обеспечить реализацию тела метода по умолчанию, в то время как trait может.
  2. Interface - полиморфная , наследуемая конструкция - в то время как trait нетт.
  3. Несколько Interface с могут быть использованы в одном классе, так же как несколько trait с.
25 голосов
/ 09 февраля 2012

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

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

18 голосов
/ 29 марта 2018

Для начинающих выше ответ может быть трудным, это самый простой способ понять это:

Черты

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

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

class MyClass{
  use SayWorld;

}

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

Круто правильно!

Не только функции, которые вы можете использовать что-либо в черту (функция, переменные,Уста ..).Также вы можете использовать несколько черт: use SayWorld,AnotherTraits;

Интерфейс

  interface SayWorld {
     public function sayHello();
  }

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

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

Надеюсь, это поможет!

4 голосов
/ 12 сентября 2013

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

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

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

4 голосов
/ 19 апреля 2012

Часто используемая метафора для описания черт: черты - это интерфейсы с реализацией.

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

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

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

3 голосов
/ 30 ноября 2012

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

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

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

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

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

2 голосов
/ 04 ноября 2017

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

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

Добавление возможностей публикации / подписки на события в класс может быть распространенным сценарием в некоторых основах кода.Существует 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 точно такой же, как и внутри *Класс 1042 *, за исключением черты, объявляет метод triggerEvent() защищенным.Таким образом, единственное различие, на которое вам нужно обратить внимание, это реализация Auction класса .

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

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

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

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

2 голосов
/ 13 сентября 2016

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...