394 votes

Traits et interfaces

J'ai essayé d'étudier le PHP dernièrement, et je me retrouve bloqué par les traits. Je comprends le concept de réutilisation horizontale du code et le fait de ne pas vouloir nécessairement hériter d'une classe abstraite. Ce que je ne comprends pas, c'est la différence cruciale entre les traits et les interfaces.

J'ai essayé de trouver 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 tellement similaires qu'ils sont identiques.

6 votes

N'ont pas de code dans les corps de fonction. En fait, elles n'ont pas de corps de fonction.

3 votes

Malgré ma réponse qui a fait l'objet de nombreux votes, j'aimerais que l'on précise que je suis généralement anti-trait/mixte . Consultez la transcription de ce chat pour lire comment les traits sapent souvent les bonnes pratiques de la POO .

2 votes

Je dirais le contraire. Ayant travaillé avec PHP pendant des années avant et depuis l'avènement des traits, je pense qu'il est facile de prouver leur valeur. Il suffit de lire cet exemple pratique qui permet aux "modèles d'image" de marcher et de parler comme des Imagick des objets, sans les lourdeurs nécessaires à l'époque des traits.

601voto

rdlowrey Points 15589

Annonce de service public :

Je tiens à préciser que je pense que les traits sont presque toujours une odeur de code et devraient être évités en faveur de la composition. A mon avis, l'héritage simple est souvent abusé au point d'être un anti-modèle et l'héritage multiple ne fait qu'aggraver ce problème. Vous serez bien mieux servi dans la plupart des cas en favorisant la composition plutôt que l'héritage (qu'il soit unique ou multiple). Si vous êtes toujours intéressé par les traits et leur relation avec les interfaces, continuez à lire ...


Commençons par dire ceci :

La programmation orientée objet (POO) peut être un paradigme difficile à appréhender. 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 la POO concerne en réalité les capacités de vos objets. Vous devez penser aux classes en termes de ce qu'elles peut faire au lieu de ce qu'ils en fait . C'est un contraste frappant avec la programmation procédurale traditionnelle, où l'accent est mis sur le fait de faire "faire quelque chose" à un bout de code.

Si le code OOP concerne la planification et la conception, une interface est le plan et un objet est la maison entièrement construite. Quant aux traits, ils sont simplement un moyen d'aider à construire la maison dessinée par le plan (l'interface).

Interfaces

Alors, pourquoi devrions-nous utiliser des interfaces ? Tout simplement parce que les interfaces rendent notre code moins fragile. Si vous doutez de cette affirmation, demandez à quiconque a été contraint de maintenir un code hérité qui n'était pas écrit avec des interfaces.

L'interface est un contrat entre le programmeur et son code. L'interface dit : "Tant que tu respectes mes règles, tu peux m'implémenter comme tu veux et je te promets que je ne casserai pas ton autre code".

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

Vous souhaitez mettre en place un système de mise en cache pour une application web afin de réduire réduire la charge du serveur

Vous commencez par écrire une classe pour mettre en cache les réponses aux requêtes en utilisant 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 vérifiez s'il y a une correspondance avec le 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 peut-être que quelques semaines plus tard, vous décidez d'utiliser un système de cache basé sur des fichiers au lieu d'APC. Vous devez alors modifier le code de votre contrôleur, car vous l'avez programmé pour qu'il fonctionne avec la fonctionnalité de l'interface de l'APC. ApcCacher plutôt qu'à une interface qui exprime les capacités de la classe ApcCacher classe. Imaginons qu'au lieu de ce qui précède, vous ayez créé la classe Controller qui dépend d'une classe CacherInterface à la place du béton ApcCacher comme ça :

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

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

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

A votre tour, vous avez vos deux ApcCacher et votre nouvelle FileCacher mettent en œuvre l'approche CacherInterface et vous programmez votre Controller pour utiliser les capacités requises par l'interface.

Cet exemple démontre (nous l'espérons) comment la programmation en fonction d'une interface vous permet de modifier l'implémentation interne de vos classes sans vous inquiéter de savoir si les changements vont casser votre autre code.

Traits

Les traits, quant à eux, sont simplement une méthode de réutilisation du code. Les interfaces ne doivent pas être considérées comme une alternative mutuellement exclusive aux traits. En effet, La création de traits qui remplissent les capacités requises par une interface est le cas d'utilisation idéal. .

Vous ne devriez utiliser les traits que lorsque plusieurs classes partagent la même fonctionnalité (probablement dictée par la même interface). Il n'y a aucun sens à utiliser un trait pour fournir une fonctionnalité à une seule classe : cela ne fait qu'obscurcir ce que fait la classe et une meilleure conception consisterait à déplacer la fonctionnalité du trait dans la classe concernée.

Considérons l'implémentation du trait suivant :

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 vos deux FileCacher et votre ApcCacher de la discussion sur l'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 (évidemment, ce n'est pas le cas dans la vie réelle, mais faites avec). Vous pourriez écrire un trait et permettre aux deux classes de l'utiliser pour répondre aux exigences de l'interface commune.

Un dernier mot d'avertissement : veillez à ne pas exagérer avec les traits. Les traits sont souvent utilisés comme une béquille pour une mauvaise conception alors que des implémentations de classes uniques suffiraient. Pour une meilleure conception du code, vous devez limiter les traits à la satisfaction des exigences de l'interface.

85 votes

Je cherchais vraiment la réponse simple et rapide qui a été fournie ci-dessus, mais je dois dire que vous avez donné une excellente réponse approfondie qui aidera à rendre la distinction plus claire pour les autres, bravo.

43 votes

"[C]réer des traits qui remplissent les capacités requises par une interface dans une classe donnée est un cas d'utilisation idéal". Exactement : +1

7 votes

Serait-il juste de dire que les traits en PHP sont similaires aux mixins dans d'autres langages ?

260voto

Alec Gorge Points 6875

Une interface définit un ensemble de méthodes que la classe d'implémentation debe mettre en œuvre.

Lorsqu'un trait est use Les implémentations des méthodes viennent aussi, ce qui n'est pas le cas dans un système de gestion de l'information. Interface .

C'est la plus grande différence.

Desde el Réutilisation horizontale pour PHP RFC :

Les Traits sont un mécanisme de réutilisation du code dans les 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.

0 votes

Pour être juste, ce n'est pas dans la documentation standard de PHP, c'est dans le rfc quand l'idée a été proposée pour être ajoutée au noyau.

1 votes

Cela ressemble exactement à une classe abstraite sans avoir à être étendue, et implémentée avec le mot clé "use". Est-ce exact ? Sinon, qu'est-ce qui ne va pas dans ce que je viens de dire ?

2 votes

@JREAM En pratique, rien. En réalité, beaucoup plus.

76voto

Troy Alford Points 8676

A trait est essentiellement l'implémentation en PHP d'un mixin Il s'agit en fait d'un ensemble de méthodes d'extension qui peuvent être ajoutées à n'importe quelle classe par l'ajout de la balise trait . Les méthodes font alors partie de l'implémentation de cette classe, mais sans utiliser l'héritage .

Desde el Manuel PHP (c'est moi qui souligne) :

Les traits sont un mécanisme pour réutilisation du code dans les langages à héritage unique tels que PHP. ... Il s'agit d'un ajout à l'héritage traditionnel qui permet la composition horizontale du comportement, c'est-à-dire l'application des membres d'une 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 il dispose de deux méthodes, appelées foo() y bar() - qui proviennent de myTrait . Et - remarquez que le trait -Les méthodes définies ont déjà un corps de méthode - ce qu'une méthode définie par l'utilisateur ne fait pas. Interface -La méthode définie par l'utilisateur ne le peut pas.

De plus, PHP, comme beaucoup d'autres langages, utilise une méthode d'écriture de type 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 ont plusieurs trait des inclusions - ce qui permet au programmeur d'inclure des éléments réutilisables - comme il pourrait le faire en incluant 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 étend SomeBaseClass , MyClass es 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 étendu IBaseInterface un tableau de IBaseInterface[] bases pourrait contenir des instances de MyClass . Il n'y a pas de construction polymorphe de ce type disponible avec un fichier trait - car un trait est essentiellement du code qui est copié pour la commodité du programmeur dans chaque classe qui l'utilise.

Priorité :

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 préséance est le suivant : les membres de la classe courante remplacent les méthodes du Trait, qui en retour remplacent les méthodes héritées.

Alors - considérez 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, le phénomène suivant se produit :

  1. El Interface IBase nécessite une fonction sans paramètre appelée SomeMethod() à fournir.
  2. La classe de base BaseClass fournit une implémentation de cette méthode - satisfaisant ainsi le besoin.
  3. El trait myTrait fournit une fonction sans paramètre appelée SomeMethod() également, qui a la priorité sur le BaseClass -version
  4. El class MyClass fournit sa propre version de SomeMethod() - qui a la priorité sur le trait -version.

Conclusion

  1. Un site Interface ne peut pas fournir une implémentation par défaut du corps d'une méthode, tandis qu'une trait peut.
  2. Un site Interface est un polymorphe , hérité de tandis qu'une trait ne l'est pas.
  3. Multiple Interface peuvent être utilisés dans la même classe, de même que de multiples trait s.

5 votes

"Un trait est similaire au concept C# de classe abstraite" Non, une classe abstraite est une classe abstraite ; ce concept existe à la fois en PHP et en C#. Je comparerais plutôt un trait en PHP à une classe statique composée de méthodes d'extension en C#, avec la restriction basée sur le type supprimée, car un trait peut être utilisé par à peu près tous les types, contrairement à une méthode d'extension qui n'étend qu'un seul type.

1 votes

Très bon commentaire - et je suis d'accord avec vous. En relisant, c'est une meilleure analogie. Je crois qu'il est encore mieux, cependant, d'y penser comme une mixin - et comme j'ai revu le début de ma réponse, j'ai mis à jour pour refléter cela. Merci de votre commentaire, @BoltClock !

1 votes

Je ne pense pas qu'il y ait de relation avec les méthodes d'extension c#. Les méthodes d'extension sont ajoutées à un type de classe unique (en respectant la hiérarchie des classes, bien sûr). Leur but est d'améliorer un type avec des fonctionnalités supplémentaires, et non de "partager du code" sur plusieurs classes et de créer un désordre. Ce n'est pas comparable ! Si quelque chose doit être réutilisé, cela signifie généralement qu'il devrait avoir son propre espace, comme une classe séparée qui serait liée aux classes qui ont besoin d'une fonctionnalité commune. La mise en œuvre peut varier en fonction de la conception, mais en gros c'est tout. Les traits sont juste un autre moyen de faire un code pauvre.

32voto

J. Bruni Points 7500

Creo que traits sont utiles pour créer des classes qui contiennent 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 utilise ce 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
    }
}

Alors qu'avec interfaces vous pouvez seulement déclarer 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 les éléments suivants implements . Ce n'est pas le cas des traits de caractère.

C'est complètement différent !

0 votes

Je pense que c'est un mauvais exemple de trait de caractère. to_integer seraient plus susceptibles d'être inclus dans un IntegerCast parce qu'il n'existe pas de moyen fondamentalement similaire de convertir (intelligemment) des classes en un nombre entier.

5 votes

Oubliez "to_integer" - c'est juste une illustration. Un exemple. Un "Hello, World". Un "exemple.com".

2 votes

Quel avantage ce trait de la boîte à outils apporte-t-il qu'une classe utilitaire autonome ne pourrait pas apporter ? Au lieu de use Toolkit vous auriez pu $this->toolkit = new Toolkit(); ou est-ce que je manque un avantage de la caractéristique elle-même ?

6voto

Jon Kloske Points 851

Une métaphore souvent utilisée pour décrire les Traits est que les Traits sont des interfaces avec une implémentation.

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

Pour commencer, le instanceof ne fonctionnera pas avec les traits (c'est-à-dire qu'un trait n'est pas un objet réel), donc vous ne pouvez pas l'utiliser pour voir si une classe a un certain trait (ou pour voir si deux classes non apparentées partagent un trait). C'est ce qu'ils veulent dire en disant que c'est une construction pour la réutilisation horizontale du code.

Il y a sont en PHP qui vous permettront d'obtenir une liste de tous les traits qu'une classe utilise, mais l'héritage des traits signifie que vous devrez faire des vérifications récursives pour vérifier de manière fiable si une classe possède un trait spécifique à un moment donné (il y a du code d'exemple sur les pages de la doco PHP). Mais oui, ce n'est certainement pas aussi simple et propre que instanceof est, et IMHO c'est une fonctionnalité qui rendrait PHP meilleur.

De plus, les classes abstraites restent des classes, donc elles ne résolvent pas les problèmes de réutilisation du code liés à l'héritage multiple. Rappelez-vous que vous ne pouvez étendre qu'une seule 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 un 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 se trouve au même endroit dans toutes les classes qui utilisent le KeyedTrait.

0 votes

La dernière partie de cette réponse correspond bien sûr à ce que @rdlowrey explique plus en détail dans les trois derniers paragraphes de la rubrique "Traits" de son message ; j'ai simplement pensé qu'un extrait de code squelette très simple permettrait de l'illustrer.

0 votes

Je pense que la meilleure façon OO d'utiliser les traits est d'utiliser les interfaces quand vous le pouvez. Et s'il y a un cas où plusieurs sous-classes implémentent le même type de code pour cette interface et que vous ne pouvez pas déplacer ce code vers leur super-classe (abstraite) -> implémentez-le avec des traits.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X