Cette page préconise la composition plutôt que l'héritage avec l'argument suivant (reformulé dans mes mots):
Un changement dans la signature d'une méthode de la superclasse (qui n'a pas été remplacée dans la sous-classe) provoque des changements supplémentaires à de nombreux endroits lorsque nous utilisons l'héritage. Cependant, lorsque nous utilisons Composition, le changement supplémentaire requis ne se fait qu'à un seul endroit: la sous-classe.
Est-ce vraiment la seule raison de privilégier la composition à l'héritage? Parce que si c'est le cas, ce problème peut être facilement résolu en appliquant un style de codage qui préconise de remplacer toutes les méthodes de la superclasse, même si la sous-classe ne change pas l'implémentation (c'est-à-dire, en mettant des remplacements factices dans la sous-classe). Est-ce que j'ai râté quelque chose?
Réponses:
J'aime les analogies, en voici une: avez-vous déjà vu un de ces téléviseurs avec lecteur de magnétoscope intégré? Que diriez-vous de celui avec un magnétoscope et un lecteur DVD? Ou celui qui a un Blu-ray, un DVD et un magnétoscope. Oh et maintenant tout le monde est en streaming donc nous devons concevoir un nouveau téléviseur ...
Très probablement, si vous possédez un téléviseur, vous n'en avez pas comme celui ci-dessus. La plupart d'entre eux n'ont probablement jamais existé. Vous avez très probablement un moniteur ou un ensemble qui a de nombreuses entrées afin que vous n'ayez pas besoin d'un nouvel ensemble chaque fois qu'il y a un nouveau type de périphérique d'entrée.
L'héritage est comme le téléviseur avec le magnétoscope intégré. Si vous avez 3 types de comportements différents que vous souhaitez utiliser ensemble et que chacun a 2 options pour l'implémentation, vous avez besoin de 8 classes différentes afin de représenter tous ces comportements. Au fur et à mesure que vous ajoutez d'autres options, les chiffres explosent. Si vous utilisez plutôt la composition, vous évitez ce problème combinatoire et la conception aura tendance à être plus extensible.
la source
Non, ça ne l'est pas. D'après mon expérience, la composition sur l'héritage est le résultat de penser votre logiciel comme un tas d' objets avec des comportements qui se parlent / des objets qui se fournissent des services au lieu de classes et données .
De cette façon, vous avez des objets qui fournissent des serviecs. Vous les utilisez comme blocs de construction (vraiment comme Lego) pour activer d'autres comportements (par exemple, un UserRegistrationService utilise les services fournis par un EmailerService et un UserRepository ).
Lors de la construction de systèmes plus grands avec cette approche, j'ai naturellement utilisé l'héritage dans très peu de cas. À la fin de la journée, l'héritage est un outil très puissant qui doit être utilisé avec prudence et uniquement lorsque cela est approprié.
la source
« Préférez composition de l' héritage » est juste une bonne heuristique
Vous devriez considérer le contexte, car ce n'est pas une règle universelle. Ne le prenez pas pour signifier ne jamais utiliser l'héritage quand vous pouvez faire de la composition. Si tel était le cas, vous le corrigeriez en interdisant l'héritage.
J'espère clarifier ce point le long de ce post.
Je n'essaierai pas de défendre le mérite de la composition par lui-même. Ce que je considère hors sujet. Au lieu de cela, je parlerai de certaines situations où le développeur peut envisager un héritage qui serait préférable d'utiliser la composition. Sur cette note, l'héritage a ses propres mérites, que je considère également hors sujet.
Exemple de véhicule
J'écris sur des développeurs qui essaient de faire les choses de façon stupide, à des fins narratives
Allons pour une variante des exemples classiques que certains cours POO utilisent ... Nous avons une
Vehicle
classe, nous dérivonsCar
,Airplane
,Balloon
etShip
.Remarque : Si vous devez mettre cet exemple à la terre, faites comme s'il s'agissait de types d'objets dans un jeu vidéo.
Ensuite,
Car
etAirplane
peuvent avoir un code commun, car ils peuvent tous les deux rouler sur terre. Les développeurs peuvent envisager de créer une classe intermédiaire dans la chaîne d'héritage pour cela. Pourtant, en fait, il existe également un code partagé entreAirplane
etBalloon
. Ils pourraient envisager de créer une autre classe intermédiaire dans la chaîne d'héritage pour cela.Ainsi, le développeur serait à la recherche d'héritage multiple. Au point où les développeurs recherchent l'héritage multiple, la conception a déjà mal tourné.
Il est préférable de modéliser ce comportement sous forme d'interfaces et de composition, afin de pouvoir le réutiliser sans avoir à exécuter l'héritage de plusieurs classes. Si les développeurs, par exemple, créent la
FlyingVehicule
classe. Ils diraient queAirplane
c'est unFlyingVehicule
(héritage de classe), mais nous pourrions plutôt dire qu'ilAirplane
a unFlying
composant (composition) etAirplane
est unIFlyingVehicule
(héritage d'interface).En utilisant des interfaces, si nécessaire, nous pouvons avoir plusieurs héritages (d'interfaces). De plus, vous n'êtes pas couplé à une implémentation particulière. Augmenter la réutilisabilité et la testabilité de votre code.
N'oubliez pas que l'héritage est un outil de polymorphisme. De plus, le polymorphisme est un outil de réutilisation. Si vous pouvez augmenter la réutilisabilité de votre code en utilisant la composition, faites-le. Si vous n'êtes pas certain que la composition offre ou non une meilleure réutilisabilité, "Préférez la composition à l'héritage" est une bonne heuristique.
Tout cela sans le mentionner
Amphibious
.En fait, nous n'avons peut-être pas besoin de choses qui décollent. Stephen Hurn a un exemple plus éloquent dans ses articles «Favoriser la composition sur l'héritage», partie 1 et partie 2 .
Substituabilité et encapsulation
Doit
A
hériter ou composerB
?Si
A
une spécialisation deB
celui-ci doit respecter le principe de substitution de Liskov , l'hérédité est viable, voire souhaitable. S'il y a des situations où ilA
n'y a pas de substitution valide,B
alors nous ne devrions pas utiliser l'héritage.Nous pourrions être intéressés par la composition comme une forme de programmation défensive, pour défendre la classe dérivée . En particulier, une fois que vous commencez à utiliser
B
à d'autres fins, il peut y avoir une pression pour le modifier ou l'étendre pour qu'il soit plus adapté à ces fins. S'il y a un risque quiB
peut exposer des méthodes qui pourraient entraîner un état invalide,A
nous devrions utiliser la composition au lieu de l'héritage. Même si nous sommes l'auteur des deuxB
etA
, c'est une chose de moins à s'inquiéter, donc la composition facilite la réutilisation deB
.Nous pouvons même affirmer que s'il y a des fonctionnalités
B
quiA
n'en ont pas besoin (et nous ne savons pas si ces fonctionnalités pourraient entraîner un état non valide pourA
, dans la mise en œuvre actuelle ou à l'avenir), c'est une bonne idée d'utiliser la composition au lieu de l'héritage.La composition présente également les avantages de permettre la mise en œuvre des commutations et de faciliter la simulation.
Remarque : il existe des situations où nous souhaitons utiliser la composition malgré la validité de la substitution. Nous archivons cette substituabilité à l'aide d'interfaces ou de classes abstraites (laquelle utiliser quand est un autre sujet), puis utilisons la composition avec injection de dépendance de l'implémentation réelle.
Enfin, bien sûr, il y a l'argument selon lequel nous devrions utiliser la composition pour défendre la classe parent parce que l'héritage rompt l'encapsulation de la classe parent:
- Modèles de conception: éléments de logiciels orientés objet réutilisables, groupe de quatre
Eh bien, c'est une classe parent mal conçue. C'est pourquoi vous devriez:
- Java efficace, Josh Bloch
Problème Yo-Yo
Un autre cas où la composition aide est le problème Yo-Yo . Ceci est une citation de Wikipedia:
Vous pouvez résoudre, par exemple: Votre classe
C
n'héritera pas de la classeB
. Au lieu de cela, votre classeC
aura un membre de typeA
, qui peut ou non être (ou avoir) un objet de typeB
. De cette façon, vous ne programmerez pas contre le détail de mise en œuvre deB
, mais contre le contrat queA
propose l'interface (de) .Exemples de compteur
De nombreux cadres favorisent l'héritage plutôt que la composition (ce qui est le contraire de ce que nous avons soutenu). Le développeur peut effectuer cette opération car il a mis beaucoup de travail dans sa classe de base que son implémentation avec la composition aurait augmenté la taille du code client. Parfois, cela est dû aux limitations de la langue.
Par exemple, un framework PHP ORM peut créer une classe de base qui utilise des méthodes magiques pour permettre l'écriture de code comme si l'objet avait des propriétés réelles. Au lieu de cela, le code géré par les méthodes magiques ira dans la base de données, recherchera le champ particulier demandé (peut-être le mettra en cache pour une demande future) et le renverra. Le faire avec la composition nécessiterait que le client crée des propriétés pour chaque champ ou écrive une version du code des méthodes magiques.
Addendum : Il existe d'autres façons de permettre l'extension des objets ORM. Ainsi, je ne pense pas que l'héritage soit nécessaire dans ce cas. C'est moins cher.
Pour un autre exemple, un moteur de jeu vidéo peut créer une classe de base qui utilise du code natif en fonction de la plate-forme cible pour effectuer le rendu 3D et la gestion des événements. Ce code est complexe et spécifique à la plate-forme. Il serait coûteux et sujet aux erreurs pour l'utilisateur développeur du moteur de gérer ce code, en fait, cela fait partie de la raison d'utiliser le moteur.
De plus, sans la partie de rendu 3D, c'est ainsi que de nombreux frameworks de widgets fonctionnent. Cela vous évite de vous soucier de la gestion des messages du système d'exploitation… en fait, dans de nombreuses langues, vous ne pouvez pas écrire un tel code sans une forme d'enchère native. De plus, si vous le faisiez, cela restreindrait votre portabilité. Au lieu de cela, avec héritage, à condition que le développeur ne casse pas la compatibilité (trop); vous pourrez à l'avenir facilement porter votre code sur toutes les nouvelles plates-formes qu'ils prennent en charge.
De plus, considérez que plusieurs fois nous ne voulons remplacer que quelques méthodes et laisser tout le reste avec les implémentations par défaut. Si nous avions utilisé la composition, nous aurions dû créer toutes ces méthodes, ne serait-ce que pour déléguer à l'objet encapsulé.
Par cet argument, il y a un point où la composition peut être pire pour la maintenabilité que l'héritage (lorsque la classe de base est trop complexe). Cependant, rappelez-vous que la maintenabilité de l'héritage peut être pire que celle de la composition (lorsque l'arbre d'héritage est trop complexe), ce à quoi je fais référence dans le problème du yo-yo.
Dans les exemples présentés, les développeurs ont rarement l'intention de réutiliser le code généré via l'héritage dans d'autres projets. Cela atténue la réutilisabilité réduite de l'utilisation de l'héritage au lieu de la composition. De plus, en utilisant l'héritage, les développeurs de framework peuvent fournir beaucoup de code facile à utiliser et à découvrir.
Dernières pensées
Comme vous pouvez le voir, la composition a un certain avantage sur l'héritage dans certaines situations, pas toujours. Il est important de considérer le contexte et les différents facteurs impliqués (tels que la réutilisabilité, la maintenabilité, la testabilité, etc.) pour prendre la décision. Retour au premier point: « Préférez composition sur l' héritage » est un juste bonne heuristique.
Vous pouvez également remarquer que la plupart des situations que je décris peuvent être résolues dans une certaine mesure avec Traits ou Mixins. Malheureusement, ce ne sont pas des fonctionnalités courantes dans la grande liste des langues, et elles s'accompagnent généralement d'un certain coût de performance. Heureusement, leurs méthodes d'extension et implémentations par défaut populaires de leurs cousins atténuent certaines situations.
J'ai un article récent où je parle de certains des avantages des interfaces dans pourquoi avons-nous besoin d'interfaces entre l'interface utilisateur, les entreprises et l'accès aux données en C # . Il aide au découplage et facilite la réutilisabilité et la testabilité, cela pourrait vous intéresser.
la source
Normalement, l'idée directrice de Composition-Over-Inheritance est de fournir une plus grande flexibilité de conception et de ne pas réduire la quantité de code que vous devez modifier pour propager un changement (que je trouve douteux).
L'exemple sur wikipedia donne un meilleur exemple de la puissance de son approche:
En définissant une interface de base pour chaque "morceau de composition", vous pourriez facilement créer divers "objets composés" avec une variété qui pourrait être difficile à atteindre avec l'héritage seul.
la source
La ligne: "Préférez la composition à l'héritage" n'est rien d'autre qu'un coup de pouce dans la bonne direction. Cela ne vous sauvera pas de votre propre stupidité et vous donnera en fait une chance d'aggraver les choses. C'est parce qu'il y a beaucoup de pouvoir ici.
La principale raison pour laquelle cela doit même être dit est que les programmeurs sont paresseux. Paresseux, ils prendront toute solution qui signifie moins de frappe au clavier. C'est le principal argument de vente de l'héritage. Je tape moins aujourd'hui et quelqu'un d'autre se fait baiser demain. Alors quoi, je veux rentrer à la maison.
Le lendemain, le patron insiste pour que j'utilise la composition. N'explique pas pourquoi mais y insiste. Je prends donc une instance de ce dont je héritais, expose une interface identique à celle dont je héritais et implémente cette interface en déléguant tout le travail à la chose dont je héritais. C'est tout un typage cérébral qui aurait pu être fait avec un outil de refactoring et qui me semble inutile mais le patron le voulait donc je le fais.
Le lendemain, je demande si c'est ce que l'on voulait. Que pensez-vous que le patron dit?
À moins qu'il ne soit nécessaire de pouvoir modifier dynamiquement (au moment de l'exécution) ce qui était hérité de (voir modèle d'état), c'était une énorme perte de temps. Comment le patron peut-il dire cela et être toujours en faveur de la composition plutôt que de l'héritage?
En plus de ne rien faire pour éviter la casse due aux changements de signature de méthode, cela manque complètement le plus grand avantage de la composition. Contrairement à l'héritage, vous êtes libre de créer une interface différente . Un plus approprié à la façon dont il sera utilisé à ce niveau. Vous savez, l'abstraction!
Il y a quelques avantages mineurs à remplacer automatiquement l'héritage par la composition et la délégation (et certains inconvénients), mais garder le cerveau éteint pendant ce processus manque une énorme opportunité.
L'indirection est très puissante. Fais-en bon usage.
la source
Voici une autre raison: dans de nombreux langages OO (je pense à ceux comme C ++, C # ou Java ici), il n'y a aucun moyen de changer la classe d'un objet une fois que vous l'avez créé.
Supposons que nous créons une base de données des employés. Nous avons une classe d'employés de base abstraite et commençons à dériver de nouvelles classes pour des rôles spécifiques - ingénieur, gardien de sécurité, chauffeur de camion, etc. Chaque classe a un comportement spécialisé pour ce rôle. Tout est bon.
Un jour, un ingénieur est promu ingénieur. Vous ne pouvez pas reclasser un ingénieur existant. Au lieu de cela, vous devez écrire une fonction qui crée un gestionnaire d'ingénierie, copiant toutes les données de l'ingénieur, supprime l'ingénieur de la base de données, puis ajoute le nouveau gestionnaire d'ingénierie. Et vous devez écrire une telle fonction pour chaque changement de rôle possible.
Pire encore, supposons qu'un chauffeur de camion part en congé de maladie de longue durée et qu'un gardien de sécurité propose de faire la conduite de camion à temps partiel pour remplir.
Il est tellement plus facile de créer une classe Employee concrète et de la traiter comme un conteneur auquel vous pouvez ajouter des propriétés, telles que le rôle du travail.
la source
L'héritage fournit deux choses:
1) Réutilisation du code: peut être accompli facilement grâce à la composition. Et en fait, est mieux réalisé grâce à la composition car il préserve mieux l'encapsulation.
2) Polymorphisme: peut être accompli via des "interfaces" (classes où toutes les méthodes sont purement virtuelles).
Un argument solide pour utiliser l'héritage non-interface est lorsque le nombre de méthodes requises est important et que votre sous-classe ne souhaite en modifier qu'un petit sous-ensemble. Cependant, en général, votre interface devrait avoir de très petites empreintes si vous souscrivez au principe de "responsabilité unique" (vous devriez), donc ces cas devraient être rares.
Le fait d'avoir le style de codage nécessitant de remplacer toutes les méthodes de classe parente n'évite pas de toute façon d'écrire un grand nombre de méthodes d'intercommunication, alors pourquoi ne pas réutiliser le code via la composition et obtenir l'avantage supplémentaire de l'encapsulation? De plus, si la super-classe devait être étendue en ajoutant une autre méthode, le style de codage nécessiterait l'ajout de cette méthode à toutes les sous-classes (et il y a de fortes chances que nous violions alors la "ségrégation d'interface" ). Ce problème se développe rapidement lorsque vous commencez à avoir plusieurs niveaux d'héritage.
Enfin, dans de nombreux langages, le test unitaire des objets basés sur la "composition" est beaucoup plus facile que le test unitaire des objets basés sur "l'héritage" en remplaçant l'objet membre par un objet factice / test / factice.
la source
Nan.
Il existe de nombreuses raisons de privilégier la composition à l'héritage. Il n'y a pas une seule bonne réponse. Mais je vais jeter une autre raison de privilégier la composition à l'héritage dans le mix:
Privilégier la composition à l'héritage améliore la lisibilité de votre code , ce qui est très important lorsque vous travaillez sur un grand projet avec plusieurs personnes.
Voici comment j'y pense: quand je lis le code de quelqu'un d'autre, si je vois qu'il a étendu une classe, je vais demander quel comportement dans la super classe change-t-il?
Et puis je vais parcourir leur code, à la recherche d'une fonction remplacée. Si je n'en vois pas, je le classe comme une mauvaise utilisation de l'héritage. Ils auraient dû utiliser la composition à la place, ne serait-ce que pour me sauver (et toutes les autres personnes de l'équipe) de 5 minutes de lecture de leur code!
Voici un exemple concret: en Java, la
JFrame
classe représente une fenêtre. Pour créer une fenêtre, vous créez une instance deJFrame
puis appelez des fonctions sur cette instance pour lui ajouter des éléments et l'afficher.De nombreux didacticiels (même les plus officiels) vous recommandent d'étendre
JFrame
afin de pouvoir traiter les instances de votre classe comme des instances deJFrame
. Mais à mon humble avis (et l'avis de beaucoup d'autres), c'est une assez mauvaise habitude à prendre! Au lieu de cela, vous devez simplement créer une instance deJFrame
et appeler des fonctions sur cette instance. Il n'est absolument pas nécessaire d'étendreJFrame
dans cette instance, car vous ne modifiez aucun des comportements par défaut de la classe parente.D'un autre côté, une façon d'effectuer une peinture personnalisée consiste à étendre la
JPanel
classe et à remplacer lapaintComponent()
fonction. C'est une bonne utilisation de l'héritage. Si je regarde dans votre code et que je vois que vous avez étenduJPanel
et remplacé lapaintComponent()
fonction, je saurai exactement quel comportement vous changez.Si c'est juste vous qui écrivez du code par vous-même, alors il pourrait être aussi facile d'utiliser l'héritage que d'utiliser la composition. Mais faites comme si vous étiez dans un environnement de groupe et que d'autres personnes liront votre code. Privilégier la composition à l'héritage facilite la lecture de votre code.
la source
new
mot-clé vous en donne un. Quoi qu'il en soit, merci pour la discussion.