Quand utiliser des traits, par opposition à l'héritage et à la composition?

9

Il existe trois méthodes courantes, AFAIK, pour implémenter la réutilisabilité en matière de POO

  1. Héritage: généralement pour représenter est une relation (un canard est un oiseau)
  2. Composition: généralement pour représenter une relation a (une voiture a un moteur)
  3. Traits (par exemple le mot-clé trait en PHP): ... pas vraiment sûr de cela

Bien qu'il me semble que les traits peuvent implémenter des relations has-a et is-a, je ne sais pas vraiment à quel type de modélisation il était destiné. Pour quel genre de situations les traits ont-ils été conçus?

Extrakun
la source
Vous pouvez expliquer un peu plus les traits, mais les traits pourraient-ils simplement être un autre mot pour les compositions?
Trilarion
1
J'aimerais vraiment savoir pourquoi aussi. Je ne les ai pas utilisés une seule fois.
Andy

Réponses:

6

Les traits sont une autre façon de composer. Considérez-les comme un moyen de composer toutes les parties d'une classe au moment de la compilation (ou du temps de compilation JIT), en assemblant les implémentations concrètes des parties dont vous aurez besoin.

Fondamentalement, vous souhaitez utiliser des traits lorsque vous vous retrouvez à créer des classes avec différentes combinaisons de fonctionnalités. Cette situation se présente le plus souvent pour les personnes qui écrivent des bibliothèques flexibles pour les autres à consommer. Par exemple, voici la déclaration d'une classe de test unitaire que j'ai écrite récemment en utilisant ScalaTest :

class TestMyClass
  extends WordSpecLike
  with Matchers
  with MyCustomTrait
  with BeforeAndAfterAll
  with BeforeAndAfterEach
  with ScalaFutures

Les frameworks de tests unitaires ont une tonne d'options de configuration différentes, et chaque équipe a des préférences différentes sur la façon dont elle veut faire les choses. En plaçant les options dans des traits (qui sont mélangés dans withScala), ScalaTest peut offrir toutes ces options sans avoir à créer des noms de classe comme WordSpecLikeWithMatchersAndFutures, ou une tonne d'indicateurs booléens d'exécution comme WordSpecLike(enableFutures, enableMatchers, ...). Cela facilite le respect du principe d'ouverture / fermeture . Vous pouvez ajouter de nouvelles fonctionnalités et de nouvelles combinaisons de fonctionnalités, simplement en ajoutant un nouveau trait. Cela facilite également le respect du principe de ségrégation d'interface , car vous pouvez facilement mettre des fonctions qui ne sont pas universellement nécessaires dans un trait.

Les traits sont également un bon moyen de mettre du code commun dans plusieurs classes qui n'ont pas de sens pour partager une hiérarchie d'héritage. L'hérédité est une relation très étroitement liée, et vous ne devriez pas payer ce coût si vous pouvez l'aider. Les traits sont une relation beaucoup plus lâche couplée. Dans mon exemple ci-dessus, j'avais l'habitude MyCustomTraitde partager facilement une implémentation de base de données fictive entre plusieurs classes de test autrement non liées.

L'injection de dépendances atteint plusieurs des mêmes objectifs, mais au moment de l'exécution en fonction des entrées de l'utilisateur plutôt qu'au moment de la compilation en fonction des entrées du programmeur. Les traits sont également destinés davantage aux dépendances qui font sémantiquement partie de la même classe. Vous êtes en quelque sorte assembler les parties d'une classe plutôt que de faire des appels à d'autres classes avec d'autres responsabilités.

Les frameworks d' injection de dépendances atteignent plusieurs des mêmes objectifs au moment de la compilation sur la base des entrées du programmeur, mais constituent en grande partie une solution de contournement pour les langages de programmation sans prise en charge appropriée des traits. Les traits apportent ces dépendances dans le domaine du vérificateur de type du compilateur, avec une syntaxe plus propre, avec un processus de construction plus simple, qui établit une distinction plus claire entre les dépendances à la compilation et à l'exécution.

Karl Bielefeldt
la source
Salut, c'est une excellente réponse. Puis-je demander comment décider quand utiliser des traits et quand utiliser l'injection de dépendance. qui semble être la même chose.
Extrakun
Observation astucieuse. Voir mon montage.
Karl Bielefeldt
Merci d'avoir écrit ceci. Je suis curieux de savoir ce que vous pensez des traits comme la composition @KarlBielefeldt. Votre classe peut être utilisée partout où ce trait est nécessaire, et vous souffririez toujours de la même fragilité qu'avec une interface régulière. Il semble donc plus proche du sous-typage et de l'héritage, non? Je ne suis pas sûr non plus de la similitude avec DI ... si vous avez défini différentes classes qui étendent les traits, alors ces dépendances concrètes ne sont-elles pas définies où que ce soit dans la hiérarchie de cette classe, plutôt qu'à un point d'entrée / haut de la code? Merci!
allstar
Les traits peuvent être utilisés comme des interfaces, pour leurs propriétés d'héritage, mais ce n'est pas ce qui les rend uniques. L' withopérateur est ce qui les différencie, withc'est une opération de composition. Et oui, les traits remplacent DI sans avoir besoin d'être définis au point d'entrée du code. C'est l'une des choses qui les rend préférables à mon avis.
Karl Bielefeldt