Pourquoi préférer la composition à l'héritage? Quels compromis y a-t-il pour chaque approche? Quand devriez-vous choisir l'héritage plutôt que la composition?
Pourquoi préférer la composition à l'héritage? Quels compromis y a-t-il pour chaque approche? Quand devriez-vous choisir l'héritage plutôt que la composition?
Réponses:
Préférez la composition à l'héritage car elle est plus malléable / facile à modifier plus tard, mais n'utilisez pas une approche de composition toujours. Avec la composition, il est facile de changer de comportement à la volée avec l'injection de dépendance / Setters. L'héritage est plus rigide car la plupart des langues ne vous permettent pas de dériver de plusieurs types. L'oie est donc plus ou moins cuite une fois issue du type A.
Mon test d'acide pour ce qui précède est:
TypeB veut-il exposer l'interface complète (toutes les méthodes publiques pas moins) de TypeA de telle sorte que TypeB puisse être utilisé là où TypeA est attendu? Indique l' héritage .
TypeB veut-il seulement une partie ou une partie du comportement exposé par TypeA? Indique le besoin de composition.
Mise à jour: Je viens de revenir à ma réponse et il semble maintenant qu'elle est incomplète sans une mention spécifique du principe de substitution Liskov de Barbara Liskov comme test pour «Dois-je hériter de ce type?
Considérez le confinement comme une relation. Une voiture "a un" moteur, une personne "a un" nom, etc.
Pensez à l' héritage comme est une relation. Une voiture "est un" véhicule, une personne "est un" mammifère, etc.
Je ne prends aucun crédit pour cette approche. Je l'ai emprunté directement à la deuxième édition de Code Complete de Steve McConnell , section 6.3 .
Si vous comprenez la différence, c'est plus facile à expliquer.
Un exemple de ceci est PHP sans l'utilisation de classes (en particulier avant PHP5). Toute logique est codée dans un ensemble de fonctions. Vous pouvez inclure d'autres fichiers contenant des fonctions d'assistance, etc., et mener votre logique métier en passant des données dans les fonctions. Cela peut être très difficile à gérer à mesure que l'application se développe. PHP5 essaie d'y remédier en proposant une conception plus orientée objet.
Cela encourage l'utilisation de classes. L'héritage est l'un des trois principes de la conception OO (héritage, polymorphisme, encapsulation).
class Person {
String Title;
String Name;
Int Age
}
class Employee : Person {
Int Salary;
String Title;
}
C'est l'héritage au travail. L'employé "est une" personne ou hérite de la personne. Toutes les relations d'héritage sont des relations «est-un». L'employé masque également la propriété Title de Person, ce qui signifie Employee.Title renvoie le titre de l'employé et non de la personne.
La composition est préférée à l'héritage. Pour le dire très simplement, vous auriez:
class Person {
String Title;
String Name;
Int Age;
public Person(String title, String name, String age) {
this.Title = title;
this.Name = name;
this.Age = age;
}
}
class Employee {
Int Salary;
private Person person;
public Employee(Person p, Int salary) {
this.person = p;
this.Salary = salary;
}
}
Person johnny = new Person ("Mr.", "John", 25);
Employee john = new Employee (johnny, 50000);
La composition est généralement «a une» ou «utilise une» relation. Ici, la classe Employee a une personne. Il n'hérite pas de Person mais obtient à la place l'objet Person qui lui est transmis, c'est pourquoi il "a" une Person.
Supposons maintenant que vous souhaitiez créer un type de gestionnaire afin de vous retrouver avec:
class Manager : Person, Employee {
...
}
Cet exemple fonctionnera bien, cependant, que se passe-t-il si la personne et l'employé ont tous deux déclaré Title
? Manager.Title doit-il retourner "Manager of Operations" ou "Mr."? Sous la composition, cette ambiguïté est mieux gérée:
Class Manager {
public string Title;
public Manager(Person p, Employee e)
{
this.Title = e.Title;
}
}
L'objet Manager est composé d'un employé et d'une personne. Le comportement Titre est emprunté à l'employé. Cette composition explicite supprime l'ambiguïté entre autres choses et vous rencontrerez moins de bugs.
Avec tous les avantages indéniables procurés par l'héritage, voici certains de ses inconvénients.
Inconvénients de l'héritage:
D'autre part, la composition des objets est définie au moment de l'exécution par le biais d'objets qui acquièrent des références à d'autres objets. Dans un tel cas, ces objets ne pourront jamais atteindre les données protégées les uns des autres (pas de rupture d'encapsulation) et seront obligés de respecter leur interface mutuelle. Et dans ce cas également, les dépendances d'implémentation seront beaucoup moins importantes qu'en cas d'héritage.
Une autre raison, très pragmatique, de préférer la composition à l'héritage est liée à votre modèle de domaine et à sa mise en correspondance avec une base de données relationnelle. Il est vraiment difficile de mapper l'héritage au modèle SQL (vous vous retrouvez avec toutes sortes de solutions de contournement hacky, comme créer des colonnes qui ne sont pas toujours utilisées, utiliser des vues, etc.). Certains ORML essaient de résoudre ce problème, mais cela se complique toujours rapidement. La composition peut être facilement modélisée grâce à une relation de clé étrangère entre deux tables, mais l'héritage est beaucoup plus difficile.
Alors que, en bref, je suis d'accord avec "Préférez la composition à l'héritage", très souvent pour moi cela ressemble à "préférez les pommes de terre au coca-cola". Il y a des lieux d'héritage et des lieux de composition. Vous devez comprendre la différence, alors cette question disparaîtra. Ce que cela signifie vraiment pour moi, c'est "si vous allez utiliser l'héritage - détrompez-vous, vous avez probablement besoin de composition".
Vous devriez préférer les pommes de terre au coca-cola lorsque vous voulez manger et le coca-cola aux pommes de terre lorsque vous voulez boire.
La création d'une sous-classe devrait signifier plus qu'un moyen pratique d'appeler des méthodes de superclasse. Vous devez utiliser l'héritage lorsque la sous-classe "est-une" super classe à la fois structurellement et fonctionnellement, lorsqu'elle peut être utilisée comme super-classe et que vous allez l'utiliser. Si ce n'est pas le cas - ce n'est pas l'héritage, mais autre chose. La composition, c'est quand vos objets se composent d'un autre, ou ont une relation avec eux.
Donc, pour moi, il semble que si quelqu'un ne sait pas s'il a besoin d'héritage ou de composition, le vrai problème est qu'il ne sait pas s'il veut boire ou manger. Pensez davantage à votre domaine problématique, comprenez-le mieux.
InternalCombustionEngine
avec une classe dérivée GasolineEngine
. Ce dernier ajoute des choses comme des bougies d'allumage, ce qui manque à la classe de base, mais en utilisant la chose comme une InternalCombustionEngine
, les bougies d'allumage seront utilisées.
L'héritage est assez attrayant, surtout en provenance de terres procédurales et il semble souvent d'une élégance trompeuse. Je veux dire, tout ce que je dois faire est d'ajouter cette fonctionnalité à une autre classe, non? Eh bien, l'un des problèmes est que
Votre classe de base rompt l'encapsulation en exposant les détails de l'implémentation à des sous-classes sous la forme de membres protégés. Cela rend votre système rigide et fragile. Le défaut le plus tragique est cependant que la nouvelle sous-classe apporte avec elle tout le bagage et l'opinion de la chaîne successorale.
L'article, L' héritage est maléfique: l'échec épique de DataAnnotationsModelBinder , passe en revue un exemple de cela en C #. Il montre l'utilisation de l'héritage quand la composition aurait dû être utilisée et comment elle pourrait être refactorisée.
En Java ou C #, un objet ne peut pas changer de type une fois qu'il a été instancié.
Donc, si votre objet doit apparaître comme un objet différent ou se comporter différemment selon l'état ou les conditions de l'objet, utilisez Composition : reportez-vous aux modèles de conception d' état et de stratégie .
Si l'objet doit être du même type, utilisez l' héritage ou implémentez des interfaces.
Client
. Ensuite, un nouveau concept de PreferredClient
pop-up apparaît plus tard. Devrait PreferredClient
hériter Client
? Un client préféré 'est un' client après tout, non? Eh bien, pas si vite ... comme vous l'avez dit, les objets ne peuvent pas changer de classe au moment de l'exécution. Comment modéliseriez-vous l' client.makePreferred()
opération? Peut-être que la réponse réside dans l'utilisation de la composition avec un concept manquant, Account
peut-être?
Client
cours, il y en a peut-être un seul qui résume le concept d'un Account
qui pourrait être un StandardAccount
ou un PreferredAccount
...
Je n'ai pas trouvé de réponse satisfaisante ici, alors j'en ai écrit une nouvelle.
Pour comprendre pourquoi " préférez la composition à l'héritage", nous devons d'abord récupérer l'hypothèse omise dans cet idiome raccourci.
L'héritage présente deux avantages: le sous-typage et le sous-classement
Le sous-typage signifie se conformer à une signature de type (interface), c'est-à-dire un ensemble d'API, et on peut remplacer une partie de la signature pour obtenir un polymorphisme de sous-typage.
La sous-classification signifie la réutilisation implicite des implémentations de méthode.
Les deux avantages apportent deux objectifs différents pour effectuer l'héritage: orienté sous-typage et orienté réutilisation de code.
Si la réutilisation du code est le seul objectif, le sous-classement peut en donner un de plus que ce dont il a besoin, c'est-à-dire que certaines méthodes publiques de la classe parent n'ont pas beaucoup de sens pour la classe enfant. Dans ce cas, au lieu de privilégier la composition à l'héritage, la composition est exigée . C'est aussi de là que vient la notion «est-un» vs «a-une».
Donc, ce n'est que lorsque le sous-typage est envisagé, c'est-à-dire pour utiliser la nouvelle classe plus tard de manière polymorphe, que nous sommes confrontés au problème du choix de l'héritage ou de la composition. C'est l'hypothèse qui est omise dans l'idiome raccourci en discussion.
Sous-taper, c'est se conformer à une signature de type, cela signifie que la composition doit toujours exposer pas moins de quantité d'API du type. Maintenant, les compromis entrent en jeu:
L'héritage fournit une réutilisation simple du code s'il n'est pas remplacé, tandis que la composition doit recoder chaque API, même s'il s'agit simplement d'un travail de délégation.
L'héritage fournit une récursivité ouverte simple via le site polymorphe interne this
, c'est-à-dire en invoquant la méthode prioritaire (ou même le type ) dans une autre fonction membre, publique ou privée (bien que découragée ). La récursivité ouverte peut être simulée via la composition , mais elle nécessite un effort supplémentaire et n'est pas toujours viable (?). Cette réponse à une question dupliquée parle de quelque chose de similaire.
L'héritage expose les membres protégés . Cela rompt l'encapsulation de la classe parente et, si elle est utilisée par la sous-classe, une autre dépendance entre l'enfant et son parent est introduite.
La composition a la forme d'une inversion de contrôle, et sa dépendance peut être injectée dynamiquement, comme le montrent le modèle décorateur et le modèle proxy .
La composition a l'avantage d' une programmation orientée combinateur , c'est-à-dire fonctionnant d'une manière similaire au modèle composite .
La composition suit immédiatement la programmation vers une interface .
La composition a l'avantage de l' héritage multiple facile .
Compte tenu des compromis ci-dessus, nous préférons donc la composition à l'héritage. Pourtant, pour les classes étroitement liées, c'est-à-dire lorsque la réutilisation implicite de code fait vraiment des bénéfices, ou lorsque la puissance magique de la récursivité ouverte est souhaitée, l'héritage doit être le choix.
Personnellement, j'ai appris à toujours préférer la composition à l'héritage. Il n'y a aucun problème de programmation que vous pouvez résoudre avec l'héritage que vous ne pouvez pas résoudre avec la composition; mais vous devrez peut-être utiliser des interfaces (Java) ou des protocoles (Obj-C) dans certains cas. Puisque C ++ ne sait rien de tel, vous devrez utiliser des classes de base abstraites, ce qui signifie que vous ne pouvez pas vous débarrasser entièrement de l'héritage en C ++.
La composition est souvent plus logique, elle fournit une meilleure abstraction, une meilleure encapsulation, une meilleure réutilisation du code (en particulier dans les très gros projets) et est moins susceptible de casser quoi que ce soit à distance simplement parce que vous avez effectué un changement isolé n'importe où dans votre code. Cela facilite également le respect du " principe de responsabilité unique ", qui est souvent résumé comme suit: " Il ne devrait jamais y avoir plus d'une raison pour qu'une classe change. ", Et cela signifie que chaque classe existe dans un but spécifique et qu'elle devrait ne disposent que de méthodes qui sont directement liées à son objectif. Le fait d'avoir un arbre d'héritage très superficiel facilite également la conservation de la vue d'ensemble même lorsque votre projet commence à devenir vraiment volumineux. Beaucoup de gens pensent que l'héritage représente notre monde réelassez bien, mais ce n'est pas la vérité. Le monde réel utilise beaucoup plus de composition que d'héritage. Presque tous les objets du monde réel que vous pouvez tenir dans votre main sont composés d'autres objets du monde réel plus petits.
Il y a cependant des inconvénients dans la composition. Si vous ignorez l'héritage et que vous vous concentrez uniquement sur la composition, vous remarquerez que vous devez souvent écrire quelques lignes de code supplémentaires qui n'étaient pas nécessaires si vous aviez utilisé l'héritage. Vous êtes également parfois obligé de vous répéter, ce qui viole le principe DRY(SEC = Ne vous répétez pas). De plus, la composition nécessite souvent une délégation, et une méthode appelle simplement une autre méthode d'un autre objet sans autre code entourant cet appel. De tels "appels de méthode double" (qui peuvent facilement s'étendre à des appels de méthode triple ou quadruple et même plus loin que cela) ont des performances bien pires que l'héritage, où vous héritez simplement d'une méthode de votre parent. L'appel d'une méthode héritée peut être aussi rapide que l'appel d'une méthode non héritée, ou il peut être légèrement plus lent, mais il est généralement toujours plus rapide que deux appels de méthode consécutifs.
Vous avez peut-être remarqué que la plupart des langages OO n'autorisent pas l'héritage multiple. Bien qu'il existe quelques cas où l'héritage multiple peut vraiment vous acheter quelque chose, mais ce sont plutôt des exceptions que la règle. Chaque fois que vous rencontrez une situation où vous pensez que "l'héritage multiple serait une fonctionnalité vraiment cool pour résoudre ce problème", vous êtes généralement à un point où vous devriez repenser complètement l'héritage, car même cela peut nécessiter quelques lignes de code supplémentaires , une solution basée sur la composition se révélera généralement beaucoup plus élégante, flexible et à l'épreuve du temps.
L'hérédité est vraiment une fonctionnalité intéressante, mais je crains qu'elle n'ait été surutilisée au cours des deux dernières années. Les gens ont traité l'héritage comme le seul marteau qui peut tout clouer, qu'il s'agisse en fait d'un clou, d'une vis ou peut-être de quelque chose de complètement différent.
TextFile
c'est un File
.
Ma règle générale: avant d'utiliser l'héritage, demandez-vous si la composition a plus de sens.
Raison: le sous-classement signifie généralement plus de complexité et de connectivité, c'est-à-dire plus difficile à modifier, à maintenir et à faire évoluer sans faire d'erreurs.
Une réponse beaucoup plus complète et concrète de Tim Boudreau de Sun:
Les problèmes communs à l'utilisation de l'héritage tel que je le vois sont:
- Des actes innocents peuvent avoir des résultats inattendus - L'exemple classique en est les appels à des méthodes redéfinissables du constructeur de la superclasse, avant que les champs d'instance des sous-classes aient été initialisés. Dans un monde parfait, personne ne ferait jamais ça. Ce n'est pas un monde parfait.
- Il offre des tentations perverses aux sous-classes de faire des hypothèses sur l'ordre des appels de méthode et autres - de telles hypothèses ont tendance à ne pas être stables si la superclasse peut évoluer avec le temps. Voir aussi mon analogie grille-pain et cafetière .
- Les classes deviennent plus lourdes - vous ne savez pas nécessairement quel travail votre superclasse fait dans son constructeur, ni combien de mémoire elle va utiliser. Donc, construire un objet léger et innocent peut être beaucoup plus cher que vous ne le pensez, et cela peut changer avec le temps si la superclasse évolue
- Il encourage une explosion de sous-classes . Le chargement de classe coûte du temps, plus de classes coûtent de mémoire. Cela peut ne pas poser de problème jusqu'à ce que vous ayez affaire à une application à l'échelle de NetBeans, mais là, nous avons eu de réels problèmes avec, par exemple, des menus lents car le premier affichage d'un menu a déclenché un chargement de classe massif. Nous avons corrigé cela en passant à une syntaxe plus déclarative et à d'autres techniques, mais cela a également coûté du temps à corriger.
- Il est plus difficile de changer les choses plus tard - si vous avez rendu une classe publique, l'échange de la superclasse va casser les sous-classes - c'est un choix qui, une fois que vous avez rendu le code public, vous êtes marié. Donc, si vous ne modifiez pas la fonctionnalité réelle de votre superclasse, vous avez beaucoup plus de liberté pour changer les choses plus tard si vous utilisez, plutôt que d'étendre la chose dont vous avez besoin. Prenez, par exemple, la sous-classe JPanel - c'est généralement faux; et si la sous-classe est publique quelque part, vous n'avez jamais la possibilité de revoir cette décision. S'il est accessible en tant que JComponent getThePanel (), vous pouvez toujours le faire (indice: exposer les modèles des composants dans votre API).
- Les hiérarchies d'objets ne sont pas mises à l'échelle (ou les faire évoluer plus tard est beaucoup plus difficile que de planifier à l'avance) - c'est le problème classique "trop de couches". J'y reviendrai ci-dessous, et comment le modèle AskTheOracle peut le résoudre (bien qu'il puisse offenser les puristes de la POO).
...
Voici ce que je dois faire, si vous autorisez l'héritage, que vous pouvez prendre avec un grain de sel:
- N'exposez aucun champ, à l'exception des constantes
- Les méthodes doivent être abstraites ou finales
- N'appelez aucune méthode depuis le constructeur de la superclasse
...
tout cela s'applique moins aux petits projets qu'aux grands, et moins aux cours privés qu'aux cours publics
Voir d'autres réponses.
On dit souvent qu'une classe Bar
peut hériter d'une classe Foo
lorsque la phrase suivante est vraie:
- un bar est un foo
Malheureusement, le test ci-dessus n'est pas fiable à lui seul. Utilisez plutôt ce qui suit:
- un bar est un foo, ET
- les bars peuvent faire tout ce que les foos peuvent faire.
Le premier test garantit que tous les getters de Foo
sens dans Bar
(= propriétés partagées), tandis que le second test s'assure que tous les setters de Foo
sens dans Bar
(= fonctionnalité partagée).
Exemple 1: Chien -> Animal
Un chien est un animal ET les chiens peuvent faire tout ce que les animaux peuvent faire (comme respirer, mourir, etc.). Par conséquent, la classe Dog
peut hériter de la classe Animal
.
Exemple 2: Cercle - / -> Ellipse
Un cercle est une ellipse MAIS les cercles ne peuvent pas faire tout ce que les ellipses peuvent faire. Par exemple, les cercles ne peuvent pas s'étirer, contrairement aux ellipses. Par conséquent, la classe Circle
ne peut pas hériter de la classe Ellipse
.
C'est ce qu'on appelle le problème Circle-Ellipse , qui n'est vraiment pas un problème, juste une preuve claire que le premier test ne suffit pas à lui seul pour conclure que l'héritage est possible. En particulier, cet exemple souligne que les classes dérivées doivent étendre la fonctionnalité des classes de base, ne jamais la restreindre . Sinon, la classe de base ne pourrait pas être utilisée de manière polymorphe.
Même si vous pouvez utiliser l'héritage ne signifie pas que vous devriez : utiliser la composition est toujours une option. L'héritage est un outil puissant permettant la réutilisation implicite du code et la répartition dynamique, mais il présente quelques inconvénients, c'est pourquoi la composition est souvent préférée. Les compromis entre héritage et composition ne sont pas évidents et, à mon avis, sont mieux expliqués dans la réponse de lcn .
En règle générale, j'ai tendance à choisir l'héritage plutôt que la composition lorsque l'utilisation polymorphe devrait être très courante, auquel cas la puissance de la répartition dynamique peut conduire à une API beaucoup plus lisible et élégante. Par exemple, avoir une classe polymorphe Widget
dans les frameworks GUI ou une classe polymorphe Node
dans les bibliothèques XML permet d'avoir une API beaucoup plus lisible et intuitive à utiliser que ce que vous auriez avec une solution purement basée sur la composition.
Juste pour que vous le sachiez, une autre méthode utilisée pour déterminer si l'héritage est possible est appelée le principe de substitution de Liskov :
Les fonctions qui utilisent des pointeurs ou des références à des classes de base doivent pouvoir utiliser des objets de classes dérivées sans le savoir
Essentiellement, cela signifie que l'héritage est possible si la classe de base peut être utilisée de manière polymorphe, ce qui, je crois, équivaut à notre test "une barre est un foo et les barres peuvent faire tout ce que les foos peuvent faire".
computeArea(Circle* c) { return pi * square(c->radius()); }
. Il est évidemment cassé s'il passe une Ellipse (que signifie même radius ()?). Une ellipse n'est pas un cercle et, en tant que telle, ne doit pas dériver du cercle.
computeArea(Circle *c) { return pi * width * height / 4.0; }
Maintenant c'est générique.
width()
et height()
? Et si maintenant un utilisateur de bibliothèque décide de créer une autre classe appelée "EggShape"? Doit-il également dériver de "Circle"? Bien sûr que non. Une forme d'oeuf n'est pas un cercle, et une ellipse n'est pas non plus un cercle, donc aucun ne devrait dériver de Circle car il casse LSP. Les méthodes exécutant une opération sur une classe Circle * font de fortes hypothèses sur ce qu'est un cercle, et briser ces hypothèses entraînera presque certainement des bogues.
L'héritage est très puissant, mais vous ne pouvez pas le forcer (voir: le problème cercle-ellipse ). Si vous ne pouvez vraiment pas être complètement sûr d'une véritable relation de sous-type "est-un", alors il est préférable d'aller avec la composition.
L'héritage crée une relation forte entre une sous-classe et une super classe; la sous-classe doit connaître les détails de mise en œuvre de la super classe. La création de la super classe est beaucoup plus difficile, quand vous devez réfléchir à la manière de l'étendre. Vous devez documenter soigneusement les invariants de classe et indiquer quelles autres méthodes les méthodes remplaçables utilisent en interne.
L'héritage est parfois utile, si la hiérarchie représente vraiment une relation is-a-relation. Il se rapporte au principe ouvert-fermé, qui stipule que les classes doivent être fermées pour modification mais ouvertes pour extension. De cette façon, vous pouvez avoir un polymorphisme; d'avoir une méthode générique qui traite du super type et de ses méthodes, mais via la répartition dynamique, la méthode de la sous-classe est invoquée. Ceci est flexible et aide à créer une indirection, ce qui est essentiel dans le logiciel (pour en savoir moins sur les détails d'implémentation).
Cependant, l'héritage est facilement surutilisé et crée une complexité supplémentaire, avec de fortes dépendances entre les classes. Il est également difficile de comprendre ce qui se passe pendant l'exécution d'un programme en raison des couches et de la sélection dynamique des appels de méthode.
Je suggère d'utiliser la composition par défaut. Il est plus modulaire et offre l'avantage d'une liaison tardive (vous pouvez changer le composant dynamiquement). Il est également plus facile de tester les choses séparément. Et si vous devez utiliser une méthode d'une classe, vous n'êtes pas obligé d'avoir une certaine forme (principe de substitution de Liskov).
Inheritance is sometimes useful... That way you can have polymorphism
comme reliant en dur les concepts d'héritage et de polymorphisme (sous-typage supposé étant donné le contexte). Mon commentaire visait à souligner ce que vous clarifiez dans votre commentaire: l'héritage n'est pas le seul moyen de mettre en œuvre le polymorphisme, et en fait n'est pas nécessairement le facteur déterminant lors du choix entre la composition et l'héritage.
Supposons qu'un avion ne comporte que deux parties: un moteur et des ailes.
Ensuite, il existe deux façons de concevoir une classe d'avion.
Class Aircraft extends Engine{
var wings;
}
Maintenant, votre avion peut commencer par avoir des ailes fixes
et les changer en ailes rotatives à la volée. C'est essentiellement
un moteur avec des ailes. Mais que se passe-t-il si je veux également changer
le moteur à la volée?
Soit la classe de base Engine
expose un mutateur pour changer ses
propriétés, soit je refonte Aircraft
comme:
Class Aircraft {
var wings;
var engine;
}
Maintenant, je peux aussi remplacer mon moteur à la volée.
Vous devez jeter un œil au principe de substitution de Liskov dans les principes SOLIDES d' oncle Bob de conception de classe. :)
Lorsque vous souhaitez "copier" / exposer l'API de la classe de base, vous utilisez l'héritage. Lorsque vous souhaitez uniquement "copier" la fonctionnalité, utilisez la délégation.
Un exemple de ceci: vous voulez créer une pile à partir d'une liste. La pile n'a que du pop, du push et du peek. Vous ne devez pas utiliser l'héritage étant donné que vous ne voulez pas de fonctionnalité push_back, push_front, removeAt, et al. Dans une pile.
Ces deux façons peuvent très bien cohabiter et se soutenir mutuellement.
La composition est simplement modulaire: vous créez une interface similaire à la classe parente, créez un nouvel objet et lui déléguez des appels. Si ces objets n'ont pas besoin de se connaître, c'est une composition assez sûre et facile à utiliser. Il y a tellement de possibilités ici.
Cependant, si la classe parent pour une raison quelconque a besoin d'accéder aux fonctions fournies par la "classe enfant" pour un programmeur inexpérimenté, il peut sembler que c'est un excellent endroit pour utiliser l'héritage. La classe parente peut simplement appeler son propre résumé "foo ()" qui est écrasé par la sous-classe et ensuite donner la valeur à la base abstraite.
Cela ressemble à une bonne idée, mais dans de nombreux cas, il vaut mieux donner à la classe un objet qui implémente foo () (ou même définir la valeur fournie foo () manuellement) que d'hériter la nouvelle classe d'une classe de base qui nécessite la fonction foo () à spécifier.
Pourquoi?
Parce que l'héritage est une mauvaise façon de déplacer des informations .
La composition a ici un réel avantage: la relation peut être inversée: la "classe parent" ou le "travailleur abstrait" peut agréger tout objet "enfant" spécifique implémentant une certaine interface + tout enfant peut être défini à l'intérieur de tout autre type de parent, qui accepte c'est du type . Et il peut y avoir n'importe quel nombre d'objets, par exemple MergeSort ou QuickSort pourrait trier n'importe quelle liste d'objets implémentant une interface de comparaison abstraite. Ou pour le dire autrement: tout groupe d'objets qui implémentent "foo ()" et tout autre groupe d'objets qui peuvent utiliser des objets ayant "foo ()" peuvent jouer ensemble.
Je peux penser à trois vraies raisons d'utiliser l'héritage:
Si cela est vrai, il est probablement nécessaire d'utiliser l'héritage.
Il n'y a rien de mal à utiliser la raison 1, c'est très bien d'avoir une interface solide sur vos objets. Cela peut être fait en utilisant la composition ou avec l'héritage, pas de problème - si cette interface est simple et ne change pas. L'héritage est généralement assez efficace ici.
Si la raison est le numéro 2, cela devient un peu délicat. Avez-vous vraiment seulement besoin d'utiliser la même classe de base? En général, le simple fait d'utiliser la même classe de base n'est pas suffisant, mais cela peut être une exigence de votre infrastructure, une considération de conception qui ne peut être évitée.
Cependant, si vous souhaitez utiliser les variables privées, le cas 3, vous pouvez avoir des problèmes. Si vous considérez que les variables globales ne sont pas sûres, alors vous devriez envisager d'utiliser l'héritage pour obtenir l'accès aux variables privées également dangereux . Attention, les variables globales ne sont pas toutes si mal - les bases de données sont essentiellement un grand ensemble de variables globales. Mais si vous pouvez le gérer, c'est très bien.
Pour répondre à cette question sous un angle différent pour les nouveaux programmeurs:
L'héritage est souvent enseigné tôt lorsque nous apprenons la programmation orientée objet, il est donc considéré comme une solution facile à un problème commun.
J'ai trois classes qui ont toutes besoin de fonctionnalités communes. Donc, si j'écris une classe de base et que j'en hérite tous, alors ils auront tous cette fonctionnalité et je n'aurai besoin que de la maintenir en un seul endroit.
Cela semble génial, mais en pratique, cela ne fonctionne presque jamais, jamais, pour plusieurs raisons:
En fin de compte, nous lions notre code dans des nœuds difficiles et n'en tirons aucun avantage, sauf que nous pouvons dire: "Cool, j'ai appris l'héritage et maintenant je l'ai utilisé." Ce n'est pas censé être condescendant, car nous l'avons tous fait. Mais nous l'avons tous fait parce que personne ne nous a dit de ne pas le faire.
Dès que quelqu'un m'a expliqué "préférez la composition à l'héritage", j'ai repensé à chaque fois que j'essayais de partager des fonctionnalités entre les classes en utilisant l'héritage et j'ai réalisé que la plupart du temps cela ne fonctionnait pas vraiment bien.
L'antidote est le principe de responsabilité unique . Considérez-le comme une contrainte. Ma classe doit faire une chose. Je dois être capable de donner à ma classe un nom qui décrit en quelque sorte cette chose qu'elle fait. (Il y a des exceptions à tout, mais les règles absolues sont parfois meilleures lorsque nous apprenons.) Il s'ensuit que je ne peux pas écrire une classe de base appelée ObjectBaseThatContainsVariousFunctionsNeededByDifferentClasses
. Quelle que soit la fonctionnalité distincte dont j'ai besoin, elle doit être dans sa propre classe, et les autres classes qui ont besoin de cette fonctionnalité peuvent dépendre de cette classe, et non en hériter.
Au risque de trop simplifier, c'est la composition - composer plusieurs classes pour travailler ensemble. Et une fois que nous avons pris cette habitude, nous constatons qu'elle est beaucoup plus flexible, maintenable et testable que l'utilisation de l'héritage.
Mis à part le / a une considération, il faut également considérer la "profondeur" de l'héritage que votre objet doit traverser. Tout ce qui dépasse cinq ou six niveaux d'héritage profond peut entraîner des problèmes de casting et de boxe / unboxing inattendus, et dans ces cas, il peut être judicieux de composer votre objet à la place.
Lorsque vous avez une relation is-a entre deux classes (par exemple, le chien est un canin), vous optez pour l'héritage.
D'un autre côté, lorsque vous avez une ou une relation adjective entre deux classes (l'élève a des cours) ou (cours de formation des enseignants), vous choisissez la composition.
Un moyen simple de comprendre cela serait que l'héritage devrait être utilisé lorsque vous avez besoin qu'un objet de votre classe ait la même interface que sa classe parent, afin qu'il puisse ainsi être traité comme un objet de la classe parent (upcasting) . De plus, les appels de fonction sur un objet de classe dérivé resteraient les mêmes partout dans le code, mais la méthode spécifique à appeler serait déterminée lors de l'exécution (c'est-à-dire que l' implémentation de bas niveau diffère, l' interface de haut niveau reste la même).
La composition doit être utilisée lorsque vous n'avez pas besoin que la nouvelle classe ait la même interface, c'est-à-dire que vous souhaitez masquer certains aspects de l'implémentation de la classe que l'utilisateur de cette classe n'a pas besoin de connaître. Ainsi, la composition est plus dans la manière de prendre en charge l' encapsulation (c'est-à-dire cacher l'implémentation) tandis que l'héritage est censé prendre en charge l' abstraction (c'est-à-dire fournir une représentation simplifiée de quelque chose, dans ce cas la même interface pour une gamme de types avec des internes différents).
Le sous-typage est approprié et plus puissant lorsque les invariants peuvent être énumérés , sinon utilisez la composition des fonctions pour l'extensibilité.
Je suis d'accord avec @ Pavel, quand il dit, il y a des lieux de composition et des lieux d'héritage.
Je pense que l'héritage devrait être utilisé si votre réponse est affirmative à l'une de ces questions.
Cependant, si votre intention est purement celle de la réutilisation du code, alors la composition est probablement un meilleur choix de conception.
L'héritage est un machanisme très puissant pour la réutilisation de code. Mais doit être utilisé correctement. Je dirais que l'héritage est utilisé correctement si la sous-classe est également un sous-type de la classe parente. Comme mentionné ci-dessus, le principe de substitution de Liskov est le point clé ici.
La sous-classe n'est pas la même chose que le sous-type. Vous pouvez créer des sous-classes qui ne sont pas des sous-types (et c'est à ce moment que vous devez utiliser la composition). Pour comprendre ce qu'est un sous-type, commençons par donner une explication de ce qu'est un type.
Lorsque nous disons que le nombre 5 est de type entier, nous déclarons que 5 appartient à un ensemble de valeurs possibles (à titre d'exemple, voir les valeurs possibles pour les types primitifs Java). Nous déclarons également qu'il existe un ensemble valide de méthodes que je peux effectuer sur la valeur comme l'addition et la soustraction. Et enfin, nous déclarons qu'il existe un ensemble de propriétés qui sont toujours satisfaites, par exemple, si j'ajoute les valeurs 3 et 5, j'obtiendrai 8 en conséquence.
Pour donner un autre exemple, pensez aux types de données abstraits, Ensemble d'entiers et Liste d'entiers, les valeurs qu'ils peuvent contenir sont limitées aux entiers. Ils prennent tous les deux en charge un ensemble de méthodes, comme add (newValue) et size (). Et ils ont tous deux des propriétés différentes (classe invariante), Sets n'autorise pas les doublons tandis que List autorise les doublons (bien sûr, il existe d'autres propriétés qu'ils satisfont tous les deux).
Le sous-type est également un type, qui a une relation avec un autre type, appelé type parent (ou supertype). Le sous-type doit satisfaire aux caractéristiques (valeurs, méthodes et propriétés) du type parent. La relation signifie que dans n'importe quel contexte où le supertype est attendu, il peut être substituable par un sous-type, sans affecter le comportement de l'exécution. Allons voir du code pour illustrer ce que je dis. Supposons que j'écris une liste d'entiers (dans une sorte de pseudo-langage):
class List {
data = new Array();
Integer size() {
return data.length;
}
add(Integer anInteger) {
data[data.length] = anInteger;
}
}
Ensuite, j'écris l'ensemble d'entiers en tant que sous-classe de la liste d'entiers:
class Set, inheriting from: List {
add(Integer anInteger) {
if (data.notContains(anInteger)) {
super.add(anInteger);
}
}
}
Notre classe Set of integers est une sous-classe de List of Integers, mais n'est pas un sous-type, car elle ne satisfait pas toutes les fonctionnalités de la classe List. Les valeurs et la signature des méthodes sont satisfaites mais les propriétés ne le sont pas. Le comportement de la méthode add (Integer) a été clairement modifié, ne préservant pas les propriétés du type parent. Pensez du point de vue du client de vos cours. Ils peuvent recevoir un ensemble d'entiers où une liste d'entiers est attendue. Le client peut vouloir ajouter une valeur et obtenir cette valeur ajoutée à la liste même si cette valeur existe déjà dans la liste. Mais elle n'obtiendra pas ce comportement si la valeur existe. Une grosse surprise pour elle!
Il s'agit d'un exemple classique d'une mauvaise utilisation de l'héritage. Utilisez la composition dans ce cas.
(un fragment de: utiliser correctement l'héritage ).
Une règle de base que j'ai entendue est que l'héritage devrait être utilisé quand c'est une relation "est-un" et sa composition quand c'est un "a-a". Même avec cela, je pense que vous devriez toujours vous pencher vers la composition, car elle élimine beaucoup de complexité.
Composition v / s L'héritage est un sujet large. Il n'y a pas de vraie réponse à ce qui est mieux car je pense que tout dépend de la conception du système.
Généralement, le type de relation entre l'objet fournit de meilleures informations pour choisir l'un d'entre eux.
Si le type de relation est une relation "IS-A", l'héritage est une meilleure approche. sinon le type de relation est une relation "HAS-A" alors la composition sera meilleure approche.
Cela dépend totalement de la relation d'entité.
Même si la composition est préférée, je voudrais souligner les avantages de l' héritage et les inconvénients de la composition .
Avantages de l'héritage:
Il établit une relation logique " EST A" . Si la voiture et le camion sont deux types de véhicules (classe de base), la classe enfant EST UNE classe de base.
c'est à dire
La voiture est un véhicule
Le camion est un véhicule
Avec l'héritage, vous pouvez définir / modifier / étendre une capacité
Inconvénients de la composition:
Par exemple, si la voiture contient un véhicule et si vous devez obtenir le prix de la voiture , qui a été défini dans le véhicule , votre code sera comme ceci
class Vehicle{
protected double getPrice(){
// return price
}
}
class Car{
Vehicle vehicle;
protected double getPrice(){
return vehicle.getPrice();
}
}
Comme beaucoup de gens l'ont dit, je vais commencer par le contrôle - s'il existe une relation «est-un». S'il existe, je vérifie généralement les points suivants:
Indique si la classe de base peut être instanciée. Autrement dit, si la classe de base peut être non abstraite. Si cela peut être non abstrait, je préfère généralement la composition
Par exemple: 1. Le comptable est un employé. Mais je n'utiliserai pas l' héritage car un objet Employee peut être instancié.
Par exemple, 2. Book est un SellingItem. Un SellingItem ne peut pas être instancié - c'est un concept abstrait. Par conséquent, je vais utiliser l'héritage. Le SellingItem est une classe de base abstraite (ou interface en C #)
Que pensez-vous de cette approche?
En outre, je soutiens la réponse @anon dans Pourquoi utiliser l'héritage?
La principale raison de l'utilisation de l'héritage n'est pas une forme de composition - c'est pour que vous puissiez obtenir un comportement polymorphe. Si vous n'avez pas besoin de polymorphisme, vous ne devriez probablement pas utiliser l'héritage.
@MatthieuM. dit dans /software/12439/code-smell-inheritance-abuse/12448#comment303759_12448
Le problème de l'héritage est qu'il peut être utilisé à deux fins orthogonales:
interface (pour le polymorphisme)
implémentation (pour la réutilisation de code)
RÉFÉRENCE
Je vois que personne n'a mentionné le problème des diamants , qui pourrait survenir avec l'héritage.
En un coup d'œil, si les classes B et C héritent de A et remplacent toutes deux la méthode X, et une quatrième classe D, héritent à la fois de B et de C, et ne remplacent pas X, quelle implémentation de XD est censée utiliser?
Wikipedia offre un bon aperçu du sujet abordé dans cette question.