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

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

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

Ответ 1

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

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

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

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

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

Ответ 2

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

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


Позвольте начать с этого:

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

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

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

Интерфейсы

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

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

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

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

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

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

Ответ 3

A 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 -defined уже имеют тело метода - метод Interface -defined не может.

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

Несколько примечаний:

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

полиморфизм:

В предыдущем примере, где MyClass extends 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, выше, происходит следующее:

  • Для Interface IBase требуется функция без параметров SomeMethod().
  • Базовый класс BaseClass обеспечивает реализацию этого метода - удовлетворяющего потребности.
  • trait myTrait также предоставляет функцию без параметров SomeMethod() , которая имеет приоритет по сравнению с BaseClass -version
  • class MyClass предоставляет свою собственную версию SomeMethod() - , которая имеет приоритет в trait -версии.

Заключение

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

Ответ 4

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

Это совсем другое!

Ответ 5

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

Черты

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, он не может иметь переменных.

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

Ответ 6

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

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

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

Теперь в PHP есть функции, которые позволят вам получить список всех признаков, которые использует класс, но trait-inheritance означает, что вам нужно будет делать рекурсивные проверки, чтобы надежно проверить, имеет ли класс в какой-то момент определенный признак (там пример кода на страницах PHP doco). Но да, это, конечно, не так просто и чисто, как instanceof, и IMHO это функция, которая сделает PHP лучше.

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

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

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

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

Ответ 7

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

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

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

Ответ 8

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

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

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

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

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

Ответ 9

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

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

Ответ 10

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

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

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

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

Ответ 11

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

Черта - это, по сути, способ "копировать и вставлять" код между классами.

Попробуйте прочитать эту статью. Что такое черты PHP?

Ответ 12

Этот признак такой же, как класс, который мы можем использовать для множественного наследования, а также для повторного использования кода.

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

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

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

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

Ответ 13

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