Au cours des derniers mois, le mantra "favoriser la composition par rapport à l'héritage" semble être né de nulle part et est devenu presque une sorte de mème au sein de la communauté de programmation. Et chaque fois que je le vois, je suis un peu mystifié. C'est comme si quelqu'un disait "privilégie les 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 que l'un était intrinsèquement supérieur à l'autre n'a aucun sens.
En outre, je ne vois jamais une explication réelle de la raison pour laquelle l' héritage est mauvais et la composition bonne, ce qui me rend encore plus méfiant. Est-il censé être accepté sur la foi? La substitution et le polymorphisme de Liskov ont des avantages bien connus et bien définis, et l’OMI comprend tout l’intérêt d’utiliser une programmation orientée objet, et personne n’explique jamais pourquoi ils devraient être écartés au profit de la composition.
Quelqu'un sait-il d'où vient ce concept et quelle en est la raison?
la source
Réponses:
Bien que je pense avoir entendu des discussions composition-héritage bien avant le GoF, je ne peux pas mettre le doigt sur une source spécifique. Cela 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 meme, destiné à l'origine à amener l'ampoule à l'illumination, est maintenant utilisé comme un club pour matraquer leur inconscient.
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, ni ne fait de la composition le fils préféré. Le fait que beaucoup de n00bs essaient d'utiliser l'héritage comme raccourci n'invalide pas le mécanisme, et presque tous n00bs tirent les leçons de l'erreur et s'améliorent de ce fait.
S'il vous plaît PENSEZ à vos conceptions, et d' arrêter des slogans jaillissant.
</ rant>
la source
Expérience.
Comme vous le dites, ce sont des outils pour différents emplois, mais l'expression a été créée 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 par la suite, tentent de l'utiliser comme moyen de réutiliser / partager du code. Le raisonnement étant "bien si j'hérite, 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éagir par héritage.
la source
Ce n'est pas une idée nouvelle, je pense qu'elle a en fait été introduite dans le livre sur les modèles de conception du GoF, publié en 1994.
Le principal problème de 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, par contre, vous ne vous préoccupez que de l'interface publique de la classe que vous composez.
Du livre GoF:
L'article de Wikipédia sur le livre de GoF a une introduction décente.
la source
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: Éléments d'un logiciel orienté objet réutilisable , publié pour la première fois en 1994. L'expression apparaît en haut de la page 20, dans l'introduction:
Ils préfacent cette déclaration par une brève comparaison de l'héritage avec la composition. Ils ne disent pas "ne jamais utiliser l'héritage".
la source
"Composition sur héritage" est un moyen court (et apparemment trompeur) de dire "Si vous pensez que les données (ou le comportement) d'une classe doivent être incorporés dans une autre classe, envisagez toujours d'utiliser la composition avant d'appliquer aveuglément l'héritage".
Pourquoi est-ce vrai? Parce que l'héritage crée un couplage étroit, au moment de la compilation, entre les 2 classes. La composition, en revanche, est un couplage lâche, qui permet notamment de séparer clairement les préoccupations, la possibilité de changer de dépendance au moment de l'exécution et de tester plus facilement et plus isolément la dépendance.
Cela signifie seulement que l'héritage doit être manipulé avec précaution, car cela a un coût et non pas que cela n'est pas utile. En fait, "Composition sur héritage" finit souvent par devenir "Composition + héritage sur héritage" puisque vous voulez souvent que votre dépendance composée soit une super-classe abstraite plutôt que la sous-classe concrète elle-même. Il vous permet de basculer entre différentes implémentations concrètes de votre dépendance lors de l'exécution.
Pour cette raison (parmi d'autres), vous verrez probablement que l'héritage est utilisé plus souvent sous la forme d'implémentation d'interface ou de classes abstraites que l'héritage vanille.
Un exemple (métaphorique) pourrait être:
"J'ai une classe Snake et je veux inclure dans cette classe ce qui se passe lorsque Snake mord. Je serais tenté de faire en sorte que Snake hérite d'une classe BiterAnimal qui utilise la méthode Bite () et remplace cette méthode pour refléter la morsure venimeuse. Mais Composition sur héritage me prévient que je devrais plutôt utiliser la composition ... Dans mon cas, cela pourrait se traduire par le fait que le serpent possède un membre Bite. La classe Bite pourrait être abstraite (ou une interface) avec plusieurs sous-classes. Par exemple, avoir des sous-classes VenomousBite et DryBite et être capable de changer de morsure sur le même exemple Snake à mesure que le serpent grandit, mais gérer tous les effets d’une Bite dans sa propre classe pourrait me permettre de le réutiliser dans cette classe Frost. , parce que le gel mord mais n’est pas un BiterAnimal, et ainsi de suite ... "
la source
Quelques arguments possibles pour la composition:
La composition est légèrement plus l'
héritage agnostique de langage / framework et ce qu'il impose / exige / permet différera entre les langages en termes de ce à quoi la sous-classe / superclasse a accès et quelles implications il peut avoir sur les méthodes virtuelles, etc. La composition est assez basique et nécessite très peu de prise en charge linguistique et, par conséquent, les implémentations sur différentes plates-formes / infrastructures peuvent partager plus facilement les modèles de composition.
La composition est un moyen 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 de la vie réelle peuvent être décomposés et composés. Supposons qu'un vélo puisse être construit en utilisant deux roues, un cadre, un siège, une chaîne, etc. Expliqué facilement par la composition. Tandis que dans une métaphore de l'héritage, on pourrait dire qu'une bicyclette étend un monocycle, ce qui est réalisable mais beaucoup plus éloigné de l'image réelle que la composition (ce n'est évidemment pas un très bon exemple d'héritage, mais le point reste le même). Même le mot héritage (du moins de la plupart des anglophones américains, je l’attendrais) invoque automatiquement une signification du type "Quelque chose d’un parent décédé" qui a une corrélation avec sa signification dans le logiciel, mais ne correspond que vaguement.
La composition est presque toujours plus flexible
En utilisant la composition, vous pouvez toujours choisir de définir votre propre comportement ou simplement d'exposer cette partie de vos parties composées. De cette manière, vous ne rencontrez aucune des restrictions pouvant être imposées par une hiérarchie d'héritage (virtuel ou non virtuel, etc.).
Donc, cela pourrait être parce que Composition est naturellement une métaphore plus simple qui a moins de contraintes théoriques que l'héritage. De plus, ces raisons particulières peuvent être plus apparentes lors de la conception, ou éventuellement se manifester lorsqu’il s’agit de résoudre certains des problèmes liés à l’héritage.
Clause de non-responsabilité:
Évidemment, ce n'est pas une 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 là que quelques-unes des raisons que l’on pourrait utiliser pour favoriser la composition.
la source
Vous venez peut-être de remarquer que des gens l'ont dit ces derniers mois, mais les bons programmeurs le savent depuis plus longtemps. Je le dis certainement, le cas échéant, depuis environ dix ans.
L'idée du concept est qu'il y a une lourde charge conceptuelle sur l'héritage. Lorsque vous utilisez l'héritage, chaque appel de méthode comporte une répartition implicite. Si vous avez des arbres d'héritage profonds, ou des envois multiples, ou (pire encore) les deux, déterminer à quel endroit la méthode particulière sera envoyée dans un appel donné peut devenir un PITA royal. Cela rend le raisonnement correct du code plus complexe et rend le débogage plus difficile.
Laissez-moi vous donner un exemple simple à illustrer. Supposons que, profondément dans un arbre d'héritage, quelqu'un ait nommé une méthode
foo
. Ensuite, quelqu'un d'autre arrive et ajoutefoo
au sommet de l'arbre, mais fait quelque chose de différent. (Ce cas est plus commun avec l'héritage multiple.) Maintenant, cette personne travaillant dans la classe racine a cassé la classe enfant obscure et ne s'en rend probablement pas compte. Vous pourriez avoir une couverture de 100% avec les tests unitaires et ne pas remarquer cette rupture car la personne située en haut ne penserait pas à tester la classe enfant, et les tests pour la classe enfant ne songent pas à tester les nouvelles méthodes créées en haut. . (Certes, il existe des moyens d'écrire des tests unitaires pour résoudre ce problème, mais il existe également des cas dans lesquels vous ne pouvez pas écrire facilement des tests de cette façon.)En revanche, lorsque vous utilisez la composition, à chaque appel, le message auquel vous envoyez l'appel est généralement plus clair. (OK, si vous utilisez l'inversion de contrôle, par exemple avec l'injection de dépendance, il peut être problématique de savoir où l'appel va aller. Mais en général, c'est plus simple à comprendre.) Cela facilite le raisonnement. En prime, la composition a pour résultat que les méthodes sont séparées les unes des autres. L'exemple ci-dessus ne devrait pas s'y produire car la classe enfant passerait à un composant obscur et il n'y a jamais de 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érentes. Bien sûr, l'héritage entraîne une surcharge conceptuelle, mais lorsqu'il est le bon outil pour le travail, il entraîne moins de surcharge conceptuelle que d'essayer de ne pas l'utiliser et de faire à la main ce qu'il fait pour vous. Personne qui sait ce qu'ils font ne dirait qu'il ne faut jamais utiliser l'héritage. Mais assurez-vous que c'est la bonne chose à faire.
Malheureusement, de nombreux développeurs en apprennent davantage sur les logiciels orientés objet, sur l'héritage, puis utilisent leur nouvel axe aussi souvent que possible. Ce qui signifie qu'ils essaient d'utiliser l'héritage où la composition était le bon outil. Espérons qu'ils apprendront mieux avec le temps, mais souvent cela ne se produit qu'après quelques membres enlevés, etc. Leur dire dès le départ que c'est une mauvaise idée a tendance à accélérer le processus d'apprentissage et à réduire le nombre de blessures.
la source
C'est une réaction à l'observation selon laquelle les débutants OO ont tendance à utiliser l'héritage lorsqu'ils n'en ont pas besoin. L'héritage n'est certes pas une mauvaise chose, mais on peut en abuser. Si une classe a simplement besoin de fonctionnalités d'une autre, la composition fonctionnera probablement. Dans d'autres circonstances, l'héritage fonctionnera et la composition ne fonctionnera pas.
Hériter d'une classe implique beaucoup de choses. Cela implique qu’une dérivée 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 est logique d’utiliser une dérivée. Il donne l'accès dérivé aux membres protégés et aux fonctions membres de Base. C'est une relation étroite, ce qui signifie qu'il a un couplage élevé, et les modifications apportées à l'une sont plus susceptibles de nécessiter des modifications à l'autre.
Le couplage est une mauvaise chose. Cela rend les programmes plus difficiles à comprendre et à modifier. Toutes choses égales par ailleurs, vous devez toujours sélectionner l'option avec moins de couplage.
Par conséquent, si la composition ou l'héritage remplit son rôle efficacement, choisissez la composition, car son couplage est faible. Si la composition ne fait pas le travail efficacement et que l'héritage le fera, choisissez l'héritage, car vous devez le faire.
la source
Voici mes deux cents (au-delà de tous les excellents points déjà soulevés):
IMHO, Cela revient au fait que la plupart des programmeurs n'obtiennent pas vraiment l'héritage et finissent par en abuser, en partie à cause de la manière dont ce concept est enseigné. Ce concept existe pour tenter de les dissuader de trop en faire, au lieu de leur apprendre à bien faire les choses.
Quiconque a consacré du temps à l'enseignement ou au mentorat sait que c'est souvent ce qui se passe, en particulier avec les nouveaux développeurs qui ont de l'expérience dans d'autres paradigmes:
Au début, ces développeurs ont le sentiment 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 éléments globaux pour implémenter des fonctionnalités communes.
Ensuite (souvent à la suite d'un enseignement trop zélé), ils découvrent les avantages de l'héritage, mais c'est souvent enseigné comme la solution miracle à la réutilisation. Ils finissent par avoir la perception que tout comportement partagé doit être partagé par héritage. En effet, l'accent est souvent mis sur l'héritage d'implémentation plutôt que sur le sous-typage.
Dans 80% des cas, c'est assez bien. Mais les 20% restants sont ceux où le problème commence. Afin d'éviter de réécrire et de s'assurer qu'ils ont bien profité du partage de l'implémentation, ils commencent à concevoir leur hiérarchie autour de l'implémentation prévue plutôt que des abstractions sous-jacentes. Le "Stack hérite de la liste doublement chaînée" en est un bon exemple.
La plupart des enseignants n'insistent pas pour l'instant sur le concept d'interface, parce que c'est un autre concept ou parce que (en C ++), vous devez les simuler en utilisant des classes abstraites et un héritage multiple qui n'est pas enseigné à ce stade. En Java, de nombreux enseignants ne distinguent pas le "pas d'héritage multiple" ou "l'héritage multiple est diabolique" de l'importance des interfaces.
Tout cela est aggravé par le fait que nous avons tellement appris qu'il est magnifique de ne pas avoir à écrire de code superflu avec l'héritage d'implémentation. Des tonnes de code de délégation simple ne semblent pas naturelles. La composition semble donc compliquée, c'est pourquoi nous avons besoin de ces règles empiriques pour pousser les nouveaux programmeurs à les utiliser (ce qu'ils font trop).
la source
Dans l'un des commentaires, Mason a mentionné qu'un jour, nous parlerions d'un 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 doit avoir une fonctionnalité.
Dans la plupart des langages OO, lorsque vous héritez d'une classe de base, vous:
Et voici devenir le problème.
Ce n’est pas la seule approche, bien que 00 langues s’y retrouvent pour la plupart. Heureusement, il existe des interfaces / classes abstraites dans celles-ci.
En outre, le manque de facilité pour faire autrement contribue à en faire une utilisation largement utilisée: franchement, même en sachant cela, hériteriez-vous d'une interface et intégreriez-vous la classe de base par composition, en déléguant la plupart des appels de méthodes? Ce serait bien mieux, vous seriez même averti si une nouvelle méthode apparaissait soudainement dans l'interface et que vous deviez choisir consciemment comment l'implémenter.
En contre-point,
Haskell
n'autorisez le principe de Liskov que pour "dériver" 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 un défaut raisonnable pour une implémentation, mais comme ils ne disposent pas de données, ce défaut ne peut être défini que par rapport aux autres méthodes proposées par le concept ou en terme de constantes.
la source
La réponse simple est: l’héritage a un couplage supérieur à la composition. Étant donné deux options, de qualités par ailleurs équivalentes, choisissez celle qui est moins couplée.
la source
Je pense que ce genre de conseil revient à dire "préférez conduire à voler". Autrement dit, les avions présentent toutes sortes d'avantages par rapport aux voitures, mais cela vient avec une certaine complexité. Donc, si beaucoup de gens essaient de voler du centre-ville vers la banlieue, le conseil dont ils ont vraiment besoin d'entendre, c'est qu'ils n'ont pas besoin de voler. En fait, voler rendra les choses plus compliquées à long terme, même si cela semble cool / efficace / facile à court terme. Alors que lorsque vous avez besoin de voler, il est généralement supposé être évident.
De même, l'héritage peut faire des choses que la composition ne peut pas, mais vous devriez l'utiliser quand vous en avez besoin, pas quand vous n'en avez pas besoin. Donc, si vous n'êtes jamais tenté d'assumer simplement que vous avez besoin d'héritage, vous n'avez pas besoin des conseils de "préférez la composition". Mais beaucoup de gens le font , et font besoin que des conseils.
C'est supposé implicite que lorsque vous AVEZ vraiment besoin d'un héritage, c'est évident, et vous devriez l'utiliser à ce moment-là.
Aussi, la réponse de Steven Lowe. Vraiment, vraiment ça.
la source
L'héritage n'est pas fondamentalement mauvais et la composition n'est pas intrinsèquement bonne. Ce ne sont que des outils qu'un programmeur orienté objet peut utiliser pour concevoir des logiciels.
Quand vous regardez une classe, est-ce qu'elle fait plus que ce qu'il devrait absolument faire ( SRP )? Est-ce une fonctionnalité de duplication inutile ( DRY ), ou est-il trop intéressé par les propriétés ou les méthodes d'autres classes ( Feature Envy ) ?. Si la classe viole tous ces concepts (et peut-être davantage), tente-t-elle d'être une classe de Dieu ? Il existe un certain nombre de problèmes qui peuvent survenir lors de la conception d'un logiciel, aucun d'entre eux n'étant nécessairement un problème d'héritage, mais pouvant rapidement créer des maux de tête importants et des dépendances fragiles dans lesquelles le polymorphisme a également été appliqué.
La source du problème est probablement moins un manque de compréhension sur l'héritage, et plus d'un choix médiocre en termes de conception, ou peut-être de ne pas reconnaître les "odeurs de code" relatives à des classes qui ne respectent pas le principe de responsabilité unique . Le polymorphisme et la substitution de Liskov ne doivent pas être écartés au profit de la composition. Le polymorphisme lui-même peut être appliqué sans recourir à l'héritage, ce sont des concepts tout à fait complémentaires. Si appliqué pensivement. L'astuce consiste à garder votre code simple et propre et à ne pas craindre d'être trop préoccupé par le nombre de classes que vous devez créer pour créer une conception système robuste.
En ce qui concerne la question de privilégier la composition au détriment de l'héritage, il ne s'agit en réalité que d'un autre cas d'application réfléchie des éléments de conception qui a le plus de sens en termes de résolution du problème. Si vous n'avez pas besoin d'hériter du comportement, alors vous ne devriez probablement pas, car la composition aidera à éviter les incompatibilités et les efforts de refactorisation majeurs qui s'imposent par la suite. Si par contre vous constatez que vous répétez beaucoup de code de telle sorte que toute la duplication soit centrée sur un groupe de classes similaires, il se peut alors que la création d'un ancêtre commun aiderait à réduire le nombre d'appels et de classes identiques. vous devrez peut-être répéter entre chaque classe. Ainsi, vous favorisez la composition, mais vous ne supposez pas que l'héritage ne soit jamais applicable.
la source
Je pense que la citation proprement dite vient de "Effective Java" de Joshua Bloch, où elle porte le titre de l'un des chapitres. Il affirme que l'héritage est sécurisé dans un package et chaque fois qu'une classe a été spécifiquement conçue et documentée pour être étendue. Il affirme, comme d'autres l'ont noté, que l'héritage rompt l'encapsulation, car une sous-classe dépend des détails de mise en œuvre de sa super-classe. Cela conduit, dit-il, à la fragilité.
Bien qu'il n'en parle pas, il me semble que l'héritage unique en Java signifie que la composition vous donne beaucoup plus de flexibilité que l'héritage.
la source