Composition sur héritage mais

13

J'essaie de m'enseigner le génie logiciel et je me heurte à des informations contradictoires qui me déroutent.

J'ai appris la POO et ce que sont les classes / interfaces abstraites et comment les utiliser, mais je lis ensuite qu'il faut «privilégier la composition plutôt que l'héritage». Je comprends que la composition, c'est quand une classe compose / crée un objet d'une autre classe pour utiliser / interagir avec les fonctionnalités de ce nouvel objet.

Donc ma question est ... Suis-je censé ne pas utiliser de classes et d'interfaces abstraites? Ne pas créer une classe abstraite et étendre / hériter la fonctionnalité de cette classe abstraite dans les classes concrètes, et au lieu de cela, simplement composer de nouveaux objets pour utiliser les fonctionnalités de l'autre classe?

Ou suis-je censé utiliser la composition ET hériter des classes abstraites; les utiliser les deux conjointement? Si oui, pourriez-vous fournir des exemples de la façon dont cela fonctionnerait et de ses avantages?

Comme je suis plus à l'aise avec PHP, je l'utilise pour améliorer mes compétences OOP / SE avant de passer à d'autres langues et de transférer mes compétences SE nouvellement acquises, donc des exemples utilisant PHP seraient les plus appréciés.

MikeMason
la source
2
"Favoriser la composition plutôt que l'héritage" est une idée idiote qui a exactement autant de sens que "privilégier les scies aux forets". Ce sont deux outils différents qu'il convient d'utiliser dans deux cas d'utilisation différents, et les moments où vous pouvez vraiment utiliser l'un ou l'autre de manière interchangeable sont en fait assez rares.
Mason Wheeler
2
"Favoriser X plutôt que Y" ne signifie pas ne jamais utiliser Y, pensez simplement si X serait meilleur
Richard Tingle
2
"Privilégier la composition plutôt que l'héritage" s'exprime plus précisément par "Jamais, jamais, jamais utiliser l'héritage de classe", avec la clarification que l'implémentation d'une interface n'est pas un héritage et si votre langue choisie ne supporte que les classes abstraites, pas les interfaces, héritant d'un 100% la classe abstraite peut également être considérée comme "non héréditaire".
David Arno
1
@MasonWheeler, il n'y a pas de cas d'utilisation valides pour l'héritage. C'est au moins une caractéristique aussi "diabolique" que "goto".
David Arno
1
@DavidArno C'est tout simplement ridicule. L'héritage est l'une des fonctionnalités les plus utiles et productives jamais développées dans toute l'histoire de la programmation. Comme pour tout ce qui est utile, il existe de nombreuses façons d'en abuser, mais utilisé correctement, il augmente considérablement la puissance et la productivité de votre travail.
Mason Wheeler

Réponses:

19

La composition sur l'héritage signifie que lorsque vous souhaitez réutiliser ou étendre les fonctionnalités d'une classe existante, il est souvent plus approprié de créer une autre classe qui «encapsulera» la classe existante et utilisera son implémentation en interne. Le motif décorateur en est un exemple.

L'héritage n'est pas un bon moyen par défaut pour gérer les scénarios de réutilisation, car vous voulez souvent utiliser seulement une partie de la fonctionnalité d'une classe de base et la sous-classe ne peut pas prendre en charge l'intégralité du contrat de la classe de base d'une manière qui satisfait la substitution Liskov principe .

La méthode de modèle est un bon exemple de cas où l'héritage est approprié.

Consultez cette réponse pour obtenir des instructions sur le choix entre la composition et l'héritage.

astreltsov
la source
+1 pour signaler qu'en cas de réutilisation partielle d'une classe, la portion inutilisée est plus proprement ignorée avec la composition qu'avec l'héritage.
Lawrence
4

Je ne connais pas assez bien PHP pour vous donner des exemples concrets dans ce langage, mais voici quelques lignes directrices que j'ai trouvées utiles.

Les interfaces / traits sont utiles pour définir le comportement à travers de nombreuses classes qui peuvent avoir très peu d'autre en commun.

Par exemple, si vous travaillez sur une API JSON, vous pouvez définir une interface qui spécifie deux méthodes: «toJson» et «toStatusCode». Cela vous permettrait d'écrire un assistant qui peut prendre n'importe quel objet qui implémente cette interface et le convertir en réponse Http.

La plupart du temps, cela est préférable à l'héritage direct, car il est moins susceptible de souffrir du problème de classe de base fragile, car il tend vers des hiérarchies de classe peu profondes.

L'héritage direct est mieux pensé comme quelque chose que vous faites lorsqu'il y a des raisons impérieuses de le faire, plutôt que comme quelque chose que vous faites à moins qu'il y ait des raisons impérieuses de ne pas le faire.

Dans les langues qui ne prennent pas en charge les implémentations par défaut dans les interfaces, cela peut être un moyen raisonnable de partager un ensemble de fonctionnalités de base, mais cela limite généralement fortement ce que vous pouvez faire avec les sous-classes (l'héritage multiple, lorsqu'il est disponible, vaut rarement la peine) . Souvent, je trouve que cela vaut la peine d'écrire les méthodes de redirection standard pour utiliser la composition à la place.

La composition doit être votre stratégie par défaut. Autant que possible, composez avec des interfaces et passez-les comme arguments de constructeur. Cela vous facilitera la vie lors de la rédaction des tests.

Évitez autant que possible de travailler avec des singletons globaux, car comparer la sortie attendue et la sortie réelle est un vrai problème si une partie de la sortie dépend de l'état de l'horloge système.

Ce n'est bien sûr pas toujours possible. Par exemple, le framework Web Play fournit une bibliothèque JSON qui est essentiellement un langage spécifique au domaine. Changer cela serait une entreprise majeure, dont le remplacement des appels à «Json.prettyPrint» ne serait qu'une très petite partie.

Morgen
la source
1

La composition est lorsqu'une classe offre des fonctionnalités en instanciant une classe (éventuellement interne) qui implémente déjà cette fonctionnalité, au lieu d'hériter de cette classe.

Ainsi, par exemple, si vous avez une classe qui modélise un navire et qu'on vous dit maintenant que votre navire devrait offrir un héliport, il n'est pas naturel de dériver votre navire d'un héliport, (duh!) À la place, vous devriez avoir votre vaisseau contient une classe d'héliport et l'expose via une Ship.getHelipad()méthode.

Autrefois (il y a une dizaine d'années), les gens considéraient l'héritage comme un moyen rapide et facile d'agréger les fonctionnalités, il y avait donc de nombreux exemples du type "navire héritant de l'héliport", qui étaient, bien sûr, très boiteux.

Mais le dicton "Favoriser la composition plutôt que l'héritage" a été soigneusement rédigé de manière à indiquer clairement qu'il ne s'agit que d'une suggestion, et non d'une règle. L'auteur du dicton a fait preuve de suffisamment de prudence pour s'abstenir de dire quelque chose comme "tu n'utiliseras jamais l' héritage, seulement la composition". Cela porte essentiellement à l'attention de la communauté du génie logiciel le fait que l'héritage a été surutilisé, alors que dans de nombreux cas, la composition produit des conceptions plus claires, élégantes et maintenables que l'héritage.

Donc, essentiellement, le dicton "Favoriser la composition sur l'héritage" suggère que chaque fois que vous êtes confronté au "hériter ou composer?" question, vous devriez réfléchir sérieusement à la stratégie la plus appropriée, et que la plupart des chances sont que la stratégie la plus appropriée se révélera être la composition, pas l'héritage.

Mais comme ce n'est pas une règle, vous devez également garder à l'esprit qu'il existe de nombreux cas où l'héritage est plus naturel. Si vous utilisez la composition là où vous auriez dû utiliser l'héritage, de nombreux maux arriveront dans votre code.

Pour revenir à l'exemple du vaisseau, si votre vaisseau a besoin d'offrir une interface pour interagir avec un FloatingMachine, alors il est plus naturel de le dériver d'une FloatingMachineclasse abstraite , qui pourrait très probablement dériver à son tour d'une autre Machineclasse abstraite .

Voici la règle générale pour la réponse à la question de la composition par rapport à l'héritage:

Ma classe a-t-elle une relation "est une" avec l'interface qu'elle doit exposer? Si oui, utilisez l'héritage. Sinon, utilisez la composition.

Un navire "est une" machine flottante, et une machine flottante "est une" machine. L'héritage est donc parfaitement bien pour ceux-là. Mais un navire n'est pas, bien sûr, un héliport. Il est donc préférable de composer la fonctionnalité de l'héliport.

Mike Nakis
la source