Au cours des derniers mois, le mantra "privilégier la composition à l'héritage" semble avoir surgi de nulle part et devenir presque une sorte de mème au sein de la communauté de la programmation. Et chaque fois que je le vois, je suis un peu mystifié. C'est comme si quelqu'un avait dit "privilégiez les exercices aux marteaux". D'après mon expérience, la composition et l'héritage sont deux outils différents avec des cas d'utilisation différents, et les traiter comme s'ils étaient interchangeables et l'un était intrinsèquement supérieur à l'autre n'a aucun sens.
De plus, je ne vois jamais d'explication réelle pour pourquoi l'héritage est mauvais et la composition est bonne, ce qui me rend encore plus méfiant. Est-il censé être simplement accepté sur la foi? La substitution et le polymorphisme de Liskov ont des avantages bien connus et clairs, et l'OMI comprend tout l'intérêt d'utiliser la programmation orientée objet, et personne n'explique jamais pourquoi ils devraient être rejetés en faveur de la composition.
Quelqu'un sait-il d'où vient ce concept et quelle en est la raison d'être?
Bien que je pense avoir entendu des discussions sur la composition et l'héritage bien avant le GoF, je ne peux pas mettre le doigt sur une source spécifique. Ça aurait pu être Booch de toute façon.
<rant>
Ah mais comme beaucoup de mantras, celui-ci a dégénéré selon des lignes typiques:
Le mème, initialement destiné à conduire les n00bs aux Lumières, est maintenant utilisé comme un club pour les matraquer sans connaissance.
La composition et l'héritage sont des choses très différentes et ne doivent pas être confondues. S'il est vrai que la composition peut être utilisée pour simuler l'héritage avec beaucoup de travail supplémentaire , cela ne fait pas de l'héritage un citoyen de seconde classe, pas plus que cela fait de la composition le fils préféré. Le fait que de nombreux n00bs essaient d'utiliser l'héritage comme raccourci n'invalide pas le mécanisme et presque tous les n00bs apprennent de l'erreur et s'améliorent ainsi.
Veuillez PENSEZ À à propos de vos créations et arrêtez de répandre des slogans.
</rant>
Expérience.
Comme vous le dites, ce sont des outils pour différents emplois, mais l'expression est venue parce que les gens ne l'utilisaient pas de cette façon.
L'héritage est avant tout un outil polymorphe, mais certaines personnes, à leurs risques et périls ultérieurs, tentent de l'utiliser comme un moyen de réutiliser/partager du code. La justification étant "eh bien si j'hérite alors j'obtiens toutes les méthodes gratuitement", mais en ignorant le fait que ces deux classes n'ont potentiellement aucune relation polymorphe.
Alors pourquoi privilégier la composition à l'héritage - enfin tout simplement parce que le plus souvent la relation entre les classes n'est pas polymorphe. Il existe simplement pour aider à rappeler aux gens de ne pas répondre aux coups de genou en héritant.
Ce n'est pas une idée nouvelle, je crois qu'elle a en fait été introduite dans le livre sur les modèles de conception du GoF, qui a été publié en 1994.
Le principal problème avec l'héritage est qu'il s'agit d'une boîte blanche. Par définition, vous devez connaître les détails d'implémentation de la classe dont vous héritez. Avec la composition, en revanche, vous ne vous souciez que de l'interface publique de la classe que vous composez.
Du livre du GoF:
L'héritage expose une sous-classe aux détails de l'implémentation de son parent, on dit souvent que "l'héritage rompt l'encapsulation"
L'article wikipedia sur le livre du GoF a une introduction décente.
Pour répondre à une partie de votre question, je pense que ce concept est apparu pour la première fois dans le livre GOF Design Patterns: Elements of Reusable Object-Oriented Software , qui a été publié pour la première fois en 1994. La phrase apparaît en haut de la page 20, dans l'introduction:
Privilégier la composition des objets à l'héritage
Ils font précéder cette déclaration d'une brève comparaison de l'héritage avec la composition. Ils ne disent pas "n'utilisez jamais l'héritage".
"Composition sur héritage" est une façon courte (et apparemment trompeuse) de dire "Lorsque vous pensez que les données (ou le comportement) d'une classe devraient être incorporés dans une autre classe, pensez toujours à utiliser la composition avant d'appliquer aveuglément l'héritage".
Pourquoi est-ce vrai? Parce que l'héritage crée un couplage serré au moment de la compilation entre les 2 classes. La composition, en revanche, est un couplage lâche, qui, entre autres, permet une séparation claire des problèmes, la possibilité de basculer entre les dépendances lors de l'exécution et une testabilité des dépendances plus facile et plus isolée.
Cela signifie uniquement que l'héritage doit être géré avec soin car il a un coût, et non qu'il n'est pas utile. En fait, "Composition sur héritage" finit souvent par être "Composition + héritage sur héritage" car vous voulez souvent que votre dépendance composée soit une superclasse abstraite plutôt que la sous-classe concrète elle-même. Il vous permet de basculer entre les différentes implémentations concrètes de votre dépendance au moment de l'exécution.
Pour cette raison (entre autres), vous verrez probablement l'héritage utilisé plus souvent sous forme d'implémentation d'interface ou de classes abstraites que l'héritage Vanilla.
Un exemple (métaphorique) pourrait être:
"J'ai une classe Snake et je veux inclure dans cette classe ce qui se passe quand le Snake mord. Je serais tenté de faire hériter le Snake d'une classe BiterAnimal qui a la méthode Bite () et de remplacer cette méthode pour refléter la morsure venimeuse . Mais Composition sur l'héritage m'avertit que je devrais essayer d'utiliser la composition à la place ... Dans mon cas, cela pourrait se traduire par le serpent ayant un membre Bite. La classe Bite pourrait être abstraite (ou une interface) avec plusieurs sous-classes. Cela permettrait moi de belles choses comme avoir les sous-classes VenomousBite et DryBite et pouvoir changer la morsure sur la même instance de Snake à mesure que le serpent grandit. De plus, gérer tous les effets d'une Bite dans sa propre classe séparée pourrait me permettre de la réutiliser dans cette classe Frost , parce que le gel mord mais n'est pas un BiterAnimal, et ainsi de suite ... "
La composition est légèrement plus indépendante du langage/du cadre
L'héritage et ce qu'il applique/requiert/active diffère entre les langues en termes de ce à quoi la sous/superclasse a accès et quelles implications en termes de performances il peut avoir par rapport aux méthodes virtuelles, etc. La composition est assez basique et nécessite très peu de langage et les implémentations sur différentes plates-formes/frameworks peuvent ainsi partager plus facilement les modèles de composition.
La composition est une manière très simple et tactile de construire des objets
L'héritage est relativement facile à comprendre, mais pas toujours aussi facilement démontré dans la vie réelle. De nombreux objets dans la vie réelle peuvent être décomposés et composés. Disons qu'un vélo peut être construit à l'aide de deux roues, d'un cadre, d'un siège, d'une chaîne, etc. S'explique facilement par la composition. Alors que dans une métaphore de l'héritage, on pourrait dire qu'une bicyclette prolonge un monocycle, quelque peu faisable mais toujours beaucoup plus loin de l'image réelle que la composition (évidemment ce n'est pas un très bon exemple d'héritage, mais le point reste le même). Même l'héritage de Word (au moins de la plupart des anglophones des États-Unis, je m'attendrais) invoque automatiquement un sens dans le sens de "quelque chose transmis d'un parent décédé" qui a une certaine corrélation avec sa signification dans le logiciel, mais ne correspond toujours que de façon lâche.
La composition est presque toujours plus flexible
En utilisant la composition, vous pouvez toujours choisir de définir votre propre comportement ou simplement exposer cette partie de vos parties composées. De cette façon, vous n'êtes confronté à aucune des restrictions qui peuvent être imposées par une hiérarchie d'héritage (virtuelle vs non virtuelle, etc.)
Donc, cela pourrait être parce que la composition est naturellement une métaphore plus simple qui a moins de contraintes théoriques que l'héritage. En outre, ces raisons particulières peuvent être plus apparentes au moment de la conception, ou même ressortir lors de la gestion de certains des points douloureux de l'hérédité.
Avertissement:
Évidemment, ce n'est pas cette rue à sens unique. Chaque conception mérite l'évaluation de plusieurs modèles/outils. L'héritage est largement utilisé, présente de nombreux avantages et est souvent plus élégant que la composition. Ce ne sont que quelques raisons possibles que l'on pourrait utiliser pour favoriser la composition.
Vous avez peut-être remarqué que des gens l'ont dit au cours des derniers mois, mais les bons programmeurs le savent depuis bien plus longtemps. Je l'ai certainement dit le cas échéant pendant environ une décennie.
Le point du concept est qu'il y a une grande surcharge conceptuelle à l'héritage. Lorsque vous utilisez l'héritage, chaque appel de méthode contient une répartition implicite. Si vous avez des arbres d'héritage profonds, ou plusieurs répartitions, ou (pire encore) les deux, alors déterminer où la méthode particulière sera répartie dans un appel particulier peut devenir un PITA royal. Cela rend le raisonnement correct sur le code plus complexe et rend le débogage plus difficile.
Permettez-moi de donner un exemple simple pour illustrer. Supposons qu'au plus profond d'un arbre d'héritage, quelqu'un ait nommé une méthode foo
. Puis quelqu'un d'autre arrive et ajoute foo
en haut de l'arborescence, mais en faisant quelque chose de différent. (Ce cas est plus fréquent avec l'héritage multiple.) Maintenant, cette personne travaillant dans la classe racine a rompu la classe enfant obscure et ne s'en rend probablement pas compte. Vous pourriez avoir une couverture à 100% avec des tests unitaires et ne pas remarquer cette rupture parce que la personne au sommet ne penserait pas à tester la classe enfant, et les tests pour la classe enfant ne pensent pas à tester les nouvelles méthodes créées au sommet . (Certes, il existe des moyens d'écrire des tests unitaires qui permettront de détecter cela, mais il y a aussi des cas où vous ne pouvez pas facilement écrire des tests de cette façon.)
En revanche, lorsque vous utilisez la composition, à chaque appel, il est généralement plus clair à quoi vous envoyez l'appel. (OK, si vous utilisez l'inversion de contrôle, par exemple avec l'injection de dépendances, alors déterminer où va l'appel peut également devenir problématique. Mais généralement, il est plus simple de le comprendre.) Cela facilite le raisonnement à ce sujet. En prime, la composition se traduit par des méthodes distinctes les unes des autres. L'exemple ci-dessus ne devrait pas s'y produire car la classe enfant se déplacerait vers un composant obscur, et il n'est jamais question de savoir si l'appel à foo
était destiné au composant obscur ou à l'objet principal.
Maintenant, vous avez absolument raison de dire que l'héritage et la composition sont deux outils très différents qui servent deux types de choses différents. Bien sûr, l'héritage entraîne des frais généraux conceptuels, mais lorsqu'il s'agit du bon outil pour le travail, il entraîne moins de frais généraux conceptuels que d'essayer de ne pas l'utiliser et de faire à la main ce qu'il fait pour vous. Personne qui sait ce qu'il fait ne dirait que vous ne devriez jamais utiliser l'héritage. Mais assurez-vous que c'est la bonne chose à faire.
Malheureusement, de nombreux développeurs découvrent les logiciels orientés objet, apprennent l'héritage, puis sortent pour utiliser leur nouvelle hache aussi souvent que possible. Ce qui signifie qu'ils essaient d'utiliser l'héritage là où la composition était le bon outil. J'espère qu'ils apprendront mieux à temps, mais cela n'arrive souvent qu'après avoir retiré quelques membres, etc. Dire à l'avance que c'est une mauvaise idée a tendance à accélérer le processus d'apprentissage et à réduire les blessures.
C'est une réaction à l'observation que OO les débutants ont tendance à utiliser l'héritage quand ils n'en ont pas besoin. L'héritage n'est certainement pas une mauvaise chose, mais il peut être surutilisé. Si une classe a juste besoin de fonctionnalités d'un autre, alors la composition fonctionnera probablement. Dans d'autres circonstances, l'héritage fonctionnera et la composition ne fonctionnera pas.
L'héritage d'une classe implique beaucoup de choses. Cela implique qu'un dérivé est un type de base (voir le principe de substitution de Liskov pour les détails sanglants), en ce sens que chaque fois que vous utilisez une base, il serait logique d'utiliser un dérivé. Il donne l'accès dérivé aux membres protégés et aux fonctions membres de Base. Il s'agit d'une relation étroite, ce qui signifie qu'elle a un couplage élevé, et les changements à l'un sont plus susceptibles d'exiger des changements à l'autre.
Le couplage est une mauvaise chose. Cela rend les programmes plus difficiles à comprendre et à modifier. Toutes choses étant égales par ailleurs, vous devez toujours sélectionner l'option avec moins de couplage.
Par conséquent, si la composition ou l'héritage fera le travail efficacement, choisissez la composition, car son couplage est plus faible. Si la composition ne fera pas le travail efficacement, et l'héritage le fera, choisissez l'héritage, car vous devez le faire.
Voici mes deux cents (au-delà de tous les excellents points déjà soulevés):
À mon humble avis, cela revient au fait que la plupart des programmeurs n'obtiennent pas vraiment d'héritage et finissent par en faire trop, en partie à cause de la façon dont ce concept est enseigné. Ce concept existe comme un moyen d'essayer de les dissuader d'en faire trop, au lieu de se concentrer sur leur apprendre à bien le faire.
Quiconque a passé du temps à enseigner ou à encadrer sait que c'est ce qui arrive souvent, en particulier avec les nouveaux développeurs qui ont de l'expérience dans d'autres paradigmes:
Ces développeurs estiment initialement que l'héritage est ce concept effrayant. Ils finissent donc par créer des types avec des chevauchements d'interface (par exemple, le même comportement spécifié sans sous-typage commun), et avec des globaux pour implémenter des fonctionnalités communes.
Ensuite (souvent à la suite d'un enseignement trop zélé), ils apprennent les avantages de l'héritage, mais il est souvent enseigné comme la solution fourre-tout à réutiliser. Ils se retrouvent avec la perception que tout comportement partagé doit être partagé par héritage. En effet, l'accent est souvent mis sur l'héritage de l'implémentation plutôt que sur le sous-typage.
Dans 80% des cas, c'est assez bon. Mais les 20% restants sont à l'origine du problème. Afin d'éviter la réécriture et pour s'assurer qu'ils ont profité du partage de l'implémentation, ils commencent à concevoir leur hiérarchie autour de l'implémentation plutôt que des abstractions sous-jacentes. La "pile hérite de la liste à double liaison" en est un bon exemple.
La plupart des enseignants n'insistent pas sur l'introduction du concept d'interfaces à ce stade, car c'est soit un autre concept, soit parce que (en C++) vous devez les simuler en utilisant des classes abstraites et l'héritage multiple qui n'est pas enseigné à ce stade. En Java, de nombreux enseignants ne distinguent pas "pas d'héritage multiple" ou "l'héritage multiple est mauvais" de l'importance des interfaces.
Tout cela est aggravé par le fait que nous avons tellement appris de la beauté de ne pas avoir à écrire du code superflu avec l'héritage de l'implémentation, que des tonnes de code de délégation simple ne semblent pas naturelles. La composition semble donc désordonnée, c'est pourquoi nous avons besoin de ces règles empiriques pour pousser les nouveaux programmeurs à les utiliser de toute façon (ce qu'ils exagèrent également).
Dans l'un des commentaires, Mason a mentionné qu'un jour nous parlerions de héritage considéré comme nuisible.
J'espere.
Le problème de l'héritage est à la fois simple et mortel, il ne respecte pas l'idée qu'un concept devrait avoir une fonctionnalité.
Dans la plupart des langages OO, lorsque vous héritez d'une classe de base, vous:
Et voici le problème.
C'est pas la seule approche, bien que les langues 00 soient généralement bloquées avec. Heureusement, des interfaces/classes abstraites existent dans celles-ci.
De plus, le manque de facilité pour faire autrement contribue à le rendre largement utilisé: franchement, même en sachant cela, hériteriez-vous d'une interface et incorporeriez-vous la classe de base par composition, en déléguant la plupart des appels de méthode? Ce serait bien mieux, vous seriez même prévenu si tout à coup une nouvelle méthode apparaissait dans l'interface et devait choisir, consciemment, comment l'implémenter.
Comme contre-point, Haskell
ne permet d'utiliser le principe de Liskov que lorsqu'il "dérive" d'interfaces pures (appelées concepts) (1). Vous ne pouvez pas dériver d'une classe existante, seule la composition vous permet d'incorporer ses données.
(1) les concepts peuvent fournir une valeur par défaut raisonnable pour une implémentation, mais comme ils n'ont pas de données, cette valeur par défaut ne peut être définie qu'en fonction des autres méthodes proposées par le concept ou en termes de constantes.
Je pense que ce genre de conseil revient à dire "préférez conduire plutôt que voler". Autrement dit, les avions ont toutes sortes d'avantages par rapport aux voitures, mais cela s'accompagne d'une certaine complexité. Donc, si beaucoup de gens essaient de voler du centre-ville vers la banlieue, le conseil qu'ils vraiment, vraiment ont besoin d'entendre est qu'ils n'ont pas besoin de voler, et en fait, voler suffira plus compliqué à long terme, même si cela semble cool/efficace/facile à court terme. Alors que lorsque vous faites devez voler, c'est généralement censé être évident.
De même, l'héritage peut faire des choses que la composition ne peut pas faire, mais vous devez l'utiliser quand vous en avez besoin, et non quand vous ne le faites pas. Donc, si vous n'êtes jamais tenté de simplement supposer que vous avez besoin d'héritage alors que vous ne le faites pas, alors vous n'avez pas besoin des conseils de "préférer la composition". Mais beaucoup de gens font, et font ont besoin de ces conseils.
Il est supposé implicitement que lorsque vous avez vraiment besoin d'héritage, c'est évident, et vous devriez alors l'utiliser.
Aussi, la réponse de Steven Lowe. Vraiment, vraiment ça.
La réponse est simple: l'héritage a un couplage plus important que la composition. Étant donné deux options, de qualités par ailleurs équivalentes, choisissez celle qui est moins couplée.
L'héritage n'est pas intrinsèquement mauvais et la composition n'est pas intrinsèquement bonne. Ce ne sont que des outils qu'un programmeur OO peut utiliser pour concevoir des logiciels.
Quand vous regardez une classe, fait-elle plus que ce qu'elle devrait absolument ( SRP )? Est-ce une duplication inutile de fonctionnalités ( SÈCHE ), ou est-ce trop intéressé par les propriétés ou méthodes d'autres classes ( Feature Envy ) ?. Si la classe viole tous ces concepts (et peut-être plus), essaie-t-elle d'être une classe de Die . Ce sont un certain nombre de problèmes qui peuvent survenir lors de la conception de logiciels, dont aucun n'est nécessairement un problème d'héritage, mais qui peuvent rapidement créer des maux de tête majeurs et des dépendances fragiles où le polymorphisme a également été appliqué.
La racine du problème est probablement moins un manque de compréhension de l'héritage, et plus l'un des mauvais choix en termes de conception, ou peut-être de ne pas reconnaître les "odeurs de code" relatives aux classes qui n'adhèrent pas au Principe de responsabilité unique. Le polymorphisme et la substitution Liskov ne doivent pas être écartés en faveur de la composition. Le polymorphisme lui-même peut être appliqué sans compter sur l'héritage, ce sont tous des concepts assez complémentaires. Si appliqué soigneusement. L'astuce consiste à garder votre code simple et propre, et à ne pas succomber à être trop préoccupé par le nombre de classes que vous devez créer afin de créer une conception de système robuste.
En ce qui concerne la question de favoriser la composition plutôt que l'héritage, ce n'est vraiment qu'un autre cas de l'application réfléchie des éléments de conception qui ont le plus de sens en termes de problème résolu. Si vous n'avez pas besoin de pour hériter du comportement, alors vous ne devriez probablement pas, car la composition aidera à éviter les incompatibilités et les efforts de refactoring majeurs plus tard. Si d'un autre côté vous constatez que vous répétez beaucoup de code de telle sorte que toute la duplication est centrée sur un groupe de classes similaires, alors il se peut que la création d'un ancêtre commun aide à réduire le nombre d'appels et de classes identiques vous devrez peut-être répéter entre chaque classe. Ainsi, vous êtes en faveur de la composition, mais vous ne supposez pas que l'héritage n'est jamais applicable.