web-dev-qa-db-fra.com

Traits vs. interfaces

J'ai essayé d'étudier PHP récemment, et je me suis retrouvé en train de me perdre sur des traits. Je comprends le concept de réutilisation de code horizontal et de ne pas vouloir hériter d'une classe abstraite. Ce que je ne comprends pas, c'est: Quelle est la différence cruciale entre l'utilisation de traits et d'interfaces?

J'ai essayé de chercher un article de blog ou un article décent expliquant quand utiliser l'un ou l'autre, mais les exemples que j'ai trouvés jusqu'à présent semblent si semblables qu'ils sont identiques.

316
datguywhowanders

Une interface définit un ensemble de méthodes que la classe d'implémentation doit implémenter.

Quand un trait est use ', les implémentations des méthodes sont également utilisées - ce qui ne se produit pas dans un Interface.

C'est la plus grande différence.

De la réutilisation horizontale pour PHP RFC :

Traits est un mécanisme de réutilisation de code dans des langages à héritage unique tels que PHP. Un trait est destiné à réduire certaines limitations de l'héritage simple en permettant à un développeur de réutiliser librement des ensembles de méthodes dans plusieurs classes indépendantes vivant dans des hiérarchies de classes différentes.

227
Alec Gorge

Annonce de service public:

Je tiens à préciser aux fins du compte rendu que, à mon avis, les traits sont presque toujours une odeur de code et devraient être évités en faveur de la composition. À mon avis, l'héritage unique fait souvent l'objet d'abus au point de devenir un anti-modèle et l'héritage multiple ne fait qu'aggraver ce problème. Vous serez beaucoup mieux servi dans la plupart des cas en privilégiant la composition par rapport à l'héritage (qu'il soit simple ou multiple). Si les traits et leur relation avec les interfaces vous intéressent toujours, lisez la suite ...


Commençons par dire ceci:

La programmation orientée objet (POO) peut être un paradigme difficile à saisir. Ce n'est pas parce que vous utilisez des classes que votre code est orienté objet (OO).

Pour écrire du code OO, vous devez comprendre que OOP concerne en réalité les capacités de vos objets. Vous devez penser aux classes en termes de ce qu'elles peuvent faire au lieu de ce qu'elles fait réellement . Cela contraste nettement avec la programmation procédurale traditionnelle où l’objectif est de créer un peu de code "faire quelque chose".

Si le code OOP concerne la planification et la conception, une interface correspond au plan directeur et un objet à la maison entièrement construite. En attendant, les traits sont simplement un moyen d'aider à construire la maison définie par le plan (l'interface).

Des interfaces

Alors, pourquoi devrions-nous utiliser des interfaces? Tout simplement, les interfaces rendent notre code moins fragile. Si vous doutez de cette affirmation, demandez à toute personne qui a été contrainte de conserver du code hérité qui n'a pas été écrit contre les interfaces.

L'interface est un contrat entre le programmeur et son code. L'interface indique: "Tant que vous respectez mes règles, vous pouvez m'implémenter comme bon vous semble et je vous promets que je ne casserai pas votre autre code."

Donc, à titre d'exemple, considérons un scénario du monde réel (sans voitures ni widgets):

Vous souhaitez implémenter un système de mise en cache pour une application Web afin de réduire la charge du serveur

Vous commencez par écrire une classe pour mettre en cache les réponses aux requêtes à l'aide d'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);
  }
}

Ensuite, dans votre objet de réponse HTTP, vous recherchez un accès au cache avant de faire tout le travail pour générer la réponse réelle:

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

Cette approche fonctionne très bien. Mais quelques semaines plus tard, vous décidez peut-être d’utiliser un système de cache basé sur des fichiers au lieu d’APC. Maintenant, vous devez changer le code de votre contrôleur car vous avez programmé votre contrôleur pour utiliser les fonctionnalités de la classe ApcCacher plutôt qu’une interface qui exprime les fonctionnalités de la classe ApcCacher. Supposons qu'au lieu de ce qui précède, vous avez rendu la classe Controller dépendant d'un CacherInterface au lieu du béton ApcCacher comme ceci:

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

Pour aller avec cela, vous définissez votre interface comme suit:

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

À tour de rôle, vos deux classes ApcCacher et vos nouvelles classes FileCacher implémentent le CacherInterface et vous programmez votre classe Controller pour qu'elle utilise les fonctionnalités requises par l'interface.

Cet exemple (espérons-le) montre comment la programmation sur une interface vous permet de modifier l'implémentation interne de vos classes sans vous soucier de savoir si les modifications briseraient votre autre code.

Traits

Les traits, en revanche, sont simplement une méthode pour réutiliser du code. Les interfaces ne doivent pas être considérées comme une alternative mutuellement exclusive aux traits. En fait, la création de traits qui remplissent les fonctionnalités requises par une interface est le cas d'utilisation idéal .

Vous ne devez utiliser des traits que lorsque plusieurs classes partagent les mêmes fonctionnalités (probablement dictées par la même interface). Il n'y a aucun sens à utiliser un trait pour fournir des fonctionnalités à une seule classe: cela obscurcit seulement ce que fait la classe et une meilleure conception déplacerait la fonctionnalité du trait dans la classe appropriée.

Considérez l'implémentation de trait suivante:

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

Un exemple plus concret: imaginez que votre FileCacher et votre ApcCacher de la discussion d’interface utilisent la même méthode pour déterminer si une entrée de cache est périmée et doit être supprimée (ce n’est évidemment pas le cas en réalité). la vie, mais allez avec elle). Vous pouvez écrire un trait et autoriser les deux classes à l'utiliser pour les besoins d'interface commune.

Un dernier mot de prudence: veillez à ne pas exagérer avec des traits. Les traits sont souvent utilisés comme béquille pour une conception médiocre, lorsque des implémentations de classe uniques suffisent. Vous devez limiter les caractéristiques à la satisfaction des exigences d'interface pour une meilleure conception du code.

507
rdlowrey

Un trait est essentiellement la mise en oeuvre par PHP d'un mixin, et constitue en réalité un ensemble de méthodes d'extension pouvant être ajoutées à toute classe par l'ajout de trait. Les méthodes deviennent alors partie intégrante de l'implémentation de cette classe, mais sans utiliser l'héritage .

De la Manuel PHP (l'emphase mienne):

Les traits sont un mécanisme de réutilisation de code dans des langages à héritage unique tels que PHP. ... C'est un ajout au patrimoine traditionnel et permet une composition horizontale du comportement; c'est-à-dire l'application des membres de la classe sans nécessiter d'héritage.

Un exemple:

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

Avec le trait ci-dessus défini, je peux maintenant faire ce qui suit:

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

À ce stade, lorsque je crée une instance de la classe MyClass, elle dispose de deux méthodes, appelées foo() et bar() - qui proviennent de myTrait. Et - notez que les méthodes définies par trait- ont déjà un corps de méthode - ce qu'une méthode définie par Interface- ne peut pas.

En outre, PHP, comme de nombreux autres langages, utilise un modèle d'héritage unique , ce qui signifie qu'une classe peut dériver de plusieurs interfaces, mais pas de plusieurs classes. Cependant, une classe PHP peut avoir plusieurs inclusions trait, ce qui permet au programmeur d'inclure des éléments réutilisables, comme s'il pouvait inclure plusieurs classes de base. .

Quelques points à noter:

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

Polymorphisme:

Dans l'exemple précédent, où MyClass s'étend SomeBaseClass, MyClass est une instance de SomeBaseClass. En d'autres termes, un tableau tel que SomeBaseClass[] bases peut contenir des instances de MyClass. De même, si MyClass extended IBaseInterface, un tableau de IBaseInterface[] bases pourrait contenir des occurrences de MyClass. Une telle construction polymorphe n'existe pas avec un trait - car un trait est essentiellement un code qui est copié pour la commodité du programmeur dans chaque classe qui l'utilise.

Préséance:

Comme décrit dans le manuel:

Un membre hérité d'une classe de base est remplacé par un membre inséré par un trait. L'ordre de priorité est que les membres de la classe actuelle remplacent les méthodes Trait, qui en retour remplacent les méthodes héritées.

Alors, considérons le scénario suivant:

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 */ }
}

Lors de la création d'une instance de MyClass, ci-dessus, les événements suivants se produisent:

  1. La InterfaceIBase nécessite une fonction sans paramètre appelée SomeMethod().
  2. La classe de base BaseClass fournit une implémentation de cette méthode - répondant au besoin.
  3. La traitmyTrait fournit également une fonction sans paramètre appelée SomeMethod() qui prime par rapport à la BaseClass-version
  4. La classMyClass fournit sa propre version de SomeMethod() - qui a priorité sur la trait -version.

Conclusion

  1. Un Interface ne peut pas fournir une implémentation par défaut d'un corps de méthode, alors qu'un trait peut le faire.
  2. Une Interface est une construction polymorphe , héritée - alors qu'une trait ne l'est pas.
  3. Plusieurs Interfaces peuvent être utilisés dans la même classe, de même que plusieurs traits.
65
Troy Alford

Je pense que traits sont utiles pour créer des classes contenant des méthodes pouvant être utilisées comme méthodes de plusieurs classes différentes.

Par exemple:

trait ToolKit
{
    public $errors = array();

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

Vous pouvez avoir et utiliser cette méthode "error" dans n'importe quelle classe qui tilise cet trait.

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
    }
}

Tandis que avec interfaces, vous ne pouvez déclarer que la signature de la méthode, mais pas le code de ses fonctions. De plus, pour utiliser une interface, vous devez suivre une hiérarchie, en utilisant implements. Ce n'est pas le cas avec les traits.

C'est complètement différent!

25
J. Bruni

Pour les débutants, la réponse ci-dessus peut être difficile. Voici le moyen le plus simple de le comprendre:

Traits

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

donc si vous voulez que sayHello fonctionne dans d'autres classes sans recréer toute la fonction, vous pouvez utiliser des traits,

class MyClass{
  use SayWorld;

}

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

Cool droit!

Non seulement les fonctions, vous pouvez utiliser n'importe quoi dans le trait (fonction, variables, const ..). vous pouvez aussi utiliser plusieurs traits: use SayWorld,AnotherTraits;

Interface

  interface SayWorld {
     public function sayHello();
  }

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

c'est donc la différence d'interface avec les traits: vous devez tout recréer dans l'interface dans la classe implémentée. l'interface n'a pas d'implémentation. et interface ne peut avoir que des fonctions et const, il ne peut pas avoir de variables.

J'espère que ça aide!

18
Supun Praneeth

ne métaphore souvent utilisée pour décrire les traits est que les traits sont des interfaces avec l'implémentation.

C'est une bonne façon de penser à cela dans la plupart des circonstances, mais il existe un certain nombre de différences subtiles entre les deux.

Pour commencer, l’opérateur instanceof ne fonctionnera pas avec les traits (c’est-à-dire qu’un trait n’est pas un objet réel). Vous ne pouvez donc pas voir si une classe a un trait particulier (ou si deux autres les classes non apparentées partagent un trait). C'est ce qu'ils entendent par concept de réutilisation de code horizontal.

Il y a = fonctions maintenant dans PHP qui vous permettront d'obtenir une liste de tous les traits utilisés par une classe, mais l'héritage de trait signifie que vous devrez faire des vérifications récursives pour vérifier de manière fiable si une classe à un moment donné a un trait spécifique (il y a un exemple de code sur les pages PHP doco). Mais oui, ce n’est certainement pas aussi simple et net qu’instanceof, et à mon humble avis, c’est une fonctionnalité qui améliorerait PHP.

De plus, les classes abstraites sont toujours des classes, elles ne résolvent donc pas les problèmes de réutilisation de code liés à plusieurs héritages. N'oubliez pas que vous ne pouvez étendre qu'une classe (réelle ou abstraite) mais implémenter plusieurs interfaces.

J'ai trouvé que les traits et les interfaces sont vraiment bons à utiliser main dans la main pour créer un pseudo héritage multiple. Par exemple:

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

Cela signifie que vous pouvez utiliser instanceof pour déterminer si l'objet Door particulier est Keyed ou non, vous savez que vous obtiendrez un ensemble cohérent de méthodes, etc., et que tout le code est au même endroit dans toutes les classes qui utilisent KeyedTrait.

4
Jon Kloske

Les traits servent simplement à la réutilisation de code .

L'interface fournit simplement la signature des fonctions devant être défini dans la classe où il peut être utilisé en fonction du choix du programmeur . Nous avons donc un prototype pour un groupe de classes .

Pour référence - http://www.php.net/manual/en/language.oop5.traits.php

4
Rajesh Paul

En gros, vous pouvez considérer un trait comme un "copier-coller" automatisé de code.

Utiliser des traits est dangereux car il n'y a aucun moyen de savoir ce qu'il fait avant l'exécution.

Cependant, les traits sont plus flexibles en raison de leur manque de limitations telles que l'héritage.

Les traits peuvent être utiles pour injecter une méthode qui enregistre quelque chose dans une classe, par exemple l'existence d'une autre méthode ou attribut. n bel article à ce sujet (mais désolé en français) .

Pour les lecteurs francophones qui peuvent l’obtenir, le GNU/Linux Magazine HS 54 contient un article sur ce sujet.

3
Benj

Si vous connaissez l'anglais et savez ce que trait signifie, c'est exactement ce que son nom l'indique. Il s'agit d'un ensemble de méthodes et de propriétés que vous associez à des classes existantes en tapant use.

Fondamentalement, vous pouvez le comparer à une seule variable. Les fonctions de fermeture peuvent use ces variables de l'extérieur de la portée et ainsi elles ont la valeur à l'intérieur. Ils sont puissants et peuvent être utilisés dans tout. Il en va de même pour les traits s'ils sont utilisés.

2
Thielicious

D'autres réponses ont très bien expliqué les différences entre les interfaces et les traits. Je vais me concentrer sur un exemple concret utile, en particulier un exemple montrant que les traits peuvent utiliser des variables d'instance, ce qui vous permet d'ajouter un comportement à une classe avec un code passe-partout minimal.

De nouveau, comme mentionné par d’autres, les traits se marient bien avec les interfaces, ce qui permet à l’interface de spécifier le contrat de comportement et le trait permettant de réaliser la mise en œuvre.

L'ajout de fonctionnalités de publication/abonnement d'événements à une classe peut être un scénario courant dans certaines bases de code. Il y a 3 solutions communes:

  1. Définissez une classe de base avec un code pub/sub événement, puis les classes qui souhaitent proposer des événements peuvent l’étendre afin d’obtenir les capacités.
  2. Définissez une classe avec le code événement pub/sub, puis les autres classes souhaitant proposer des événements peuvent l’utiliser via la composition, en définissant leurs propres méthodes pour envelopper l’objet composé, en effectuant un proxy pour les appels de méthode.
  3. Définissez un trait avec un événement pub/sub code, puis les autres classes qui souhaitent proposer des événements peuvent use le trait, c'est-à-dire l'importer, pour obtenir les capacités.

Dans quelle mesure chacun fonctionne-t-il?

# 1 ne fonctionne pas bien. Jusqu'au jour où vous réaliserez que vous ne pouvez pas étendre la classe de base car vous étendez déjà quelque chose d'autre. Je ne montrerai pas d'exemple car il devrait être évident combien il est difficile d'utiliser un tel héritage.

Les n ° 2 et n ° 3 fonctionnent bien. Je vais montrer un exemple qui met en évidence certaines différences.

Premièrement, un code qui sera identique entre les deux exemples:

Une interface

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

Et du code pour démontrer l'utilisation:

$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());
}

Ok, voyons maintenant en quoi l’implémentation de la classe Auction sera différente lors de l’utilisation de traits.

Premièrement, voici à quoi ressemblerait le n ° 2 (avec composition):

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);
    }
}

Voici à quoi ressemblerait le # 3 (traits):

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]);
    }
}

Notez que le code à l'intérieur de EventEmitterTrait est exactement le même que celui à l'intérieur de la classe EventEmitter sauf que le trait déclare la méthode triggerEvent() comme protégée. Donc, la seule différence que vous devez regarder est l’implémentation de la classe Auction.

Et la différence est grande. En utilisant la composition, nous obtenons une excellente solution, nous permettant de réutiliser notre EventEmitter par autant de classes que nous le souhaitons. Mais le principal inconvénient est que nous avons beaucoup de code passe-partout que nous devons écrire et maintenir car, pour chaque méthode définie dans l'interface Observable, nous devons l'implémenter et écrire un code warmplate ennuyeux qui ne fait que transmettre les arguments sur la méthode correspondante dans notre composé l'objet EventEmitter. Utiliser le trait de cet exemple nous permet d'éviter cela, en nous aidant réduire le code passe-partout et améliorer la maintenabilité.

Cependant, il peut arriver que vous ne vouliez pas que votre classe Auction implémente l’interface complète Observable - vous ne voulez peut-être exposer qu’une ou deux méthodes, voire aucune, de sorte que pouvez définir vos propres signatures de méthode. Dans ce cas, vous préférerez peut-être toujours utiliser la méthode de composition.

Mais le trait est très convaincant dans la plupart des scénarios, en particulier si l'interface a beaucoup de méthodes, ce qui vous oblige à écrire beaucoup de passe-passe.

* Vous pouvez en fait faire les deux: définissez la classe EventEmitter au cas où vous souhaiteriez l'utiliser par la composition et définissez également le trait EventEmitterTrait à l'aide de l'implémentation de la classe EventEmitter à l'intérieur du trait: )

2
goat

Une interface est un contrat qui dit “cet objet est capable de faire cette chose”, alors qu'un trait donne à l'objet la capacité de faire la chose.

Un trait est essentiellement un moyen de "copier-coller" du code entre les classes.

Essayez de lire cet article, Quels sont les traits PHP?

1
Hos Mercury

Le trait est identique à une classe que nous pouvons utiliser à des fins d'héritage multiples et également de réutilisation du code.

Nous pouvons utiliser des traits à l'intérieur de la classe et également utiliser plusieurs traits dans la même classe avec 'use keyword'.

L'interface utilise pour la réutilisation du code la même chose qu'un trait

l'interface est l'extension de plusieurs interfaces afin que nous puissions résoudre les problèmes d'héritage multiples, mais lorsque nous implémentons l'interface, nous devons créer toutes les méthodes de la classe. Pour plus d'informations, cliquez sur le lien ci-dessous:

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

1
Chirag Prajapati

La principale différence est que, avec les interfaces, vous devez définir l'implémentation réelle de chaque méthode dans chaque classe qui implémente ladite interface. Vous pouvez ainsi avoir plusieurs classes implémentant la même interface mais avec un comportement différent, alors que les traits ne sont que des fragments de code injectés. une classe; Une autre différence importante est que les méthodes de trait ne peuvent être que des méthodes de classe ou des méthodes statiques, contrairement aux méthodes d'interface qui peuvent également (et sont généralement) des méthodes d'instance.

0
Alessandro Martin