Черты характера против Интерфейсы


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

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

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

Author: Rahil Wazir, 2012-02-09

13 answers

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

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

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

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

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

 201
Author: Alec Gorge, 2016-01-19 22:41:56

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

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


Давайте начнем с того, что скажем следующее:

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

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

 452
Author: rdlowrey, 2017-02-15 20:33:42

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

Приоритет:

Как описано в Руководстве:

Унаследованный элемент от базового класса переопределяется элементом, вставленным Признаком. Порядок приоритета заключается в том, что члены текущего класса переопределяют методы признаков, которые, в свою очередь, переопределяют унаследованные методы.

Итак, рассмотрим следующий сценарий:

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.
 57
Author: Troy Alford, 2014-01-05 00:09:20

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

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

 22
Author: J. Bruni, 2013-08-15 12:11:01

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

Черты характера

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

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

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

 6
Author: Supun Praneeth, 2018-06-11 05:54:45

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

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

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

В PHP теперь есть функции , которые позволят вам получить список всех признаков, используемых классом, но наследование признаков означает, что вам нужно будет выполнить рекурсивные проверки, чтобы надежно проверить, есть ли у класса в какой-то момент определенная черта (на страницах документации PHP есть пример кода). Но да, это, конечно, не так просто и чисто, как instanceof, и ИМХО это функция, которая сделает PHP лучше.

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

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

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

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

 3
Author: Jon Kloske, 2012-04-19 05:21:22

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

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

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

 3
Author: Rajesh Paul, 2013-09-12 19:25:16

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

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

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

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

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

 2
Author: Benj, 2012-11-30 17:49:10

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

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

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

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

 2
Author: goat, 2017-11-04 05:22:51

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

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

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

 1
Author: Hos Mercury, 2015-10-18 01:53:35

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

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

 1
Author: Thielicious, 2016-09-13 11:37:52

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

 0
Author: Alessandro Martin, 2013-09-16 05:40:39

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

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

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

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

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

 0
Author: Chirag Prajapati, 2018-06-05 17:09:24