Vous préférez la composition à l'héritage?

1604

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?

Lecture seulement
la source
40
Il y a un bon article sur cette question ici . Mon opinion personnelle est qu'il n'y a pas de principe "meilleur" ou "pire" à concevoir. Il existe une conception «appropriée» et «inadéquate» pour la tâche concrète. En d'autres termes - j'utilise à la fois l'héritage ou la composition, selon la situation. L'objectif est de produire du code plus petit, plus facile à lire, à réutiliser et éventuellement à étendre.
m_pGladiator
1
dans une phrase, l'héritage est public si vous avez une méthode publique et que vous la modifiez elle modifie l'api publiée. si vous avez une composition et que l'objet composé a changé, vous n'avez pas besoin de changer votre API publiée.
Tomer Ben David du

Réponses:

1189

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 .

    • Par exemple, un biplan Cessna exposera l'interface complète d'un avion, sinon plus. Donc, cela le rend apte à dériver de l'avion.
  • TypeB veut-il seulement une partie ou une partie du comportement exposé par TypeA? Indique le besoin de composition.

    • Par exemple, un oiseau peut avoir besoin uniquement du comportement de vol d'un avion. Dans ce cas, il est logique de l'extraire en tant qu'interface / classe / both et d'en faire un membre des deux classes.

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?

Gishu
la source
81
Le deuxième exemple est directement tiré du livre Head First Design Patterns ( amazon.com/First-Design-Patterns-Elisabeth-Freeman/dp/… ) :) Je recommanderais vivement ce livre à quiconque googlerait cette question.
Jeshurun
4
C'est très clair, mais il peut manquer quelque chose: "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?" Mais que se passe-t-il si cela est vrai et que TypeB expose également l'interface complète de TypeC? Et si TypeC n'a pas encore été modélisé?
Tristan
4
Vous faites allusion à ce que je pense être le test le plus élémentaire: "Cet objet devrait-il être utilisable par du code qui attend des objets de (quel serait) le type de base". Si la réponse est oui, l'objet doit hériter. Si non, alors il ne devrait probablement pas. Si j'avais mes druthers, les langues fourniraient un mot-clé pour faire référence à "cette classe", et fourniraient un moyen de définir une classe qui devrait se comporter comme une autre classe, mais ne pas être substituable à elle (une telle classe aurait tout "ceci class "références remplacées par lui-même).
supercat
22
@Alexey - le point est «Puis-je passer dans un biplan Cessna à tous les clients qui attendent un avion sans les surprendre?». Si oui, il y a de fortes chances que vous souhaitiez l'héritage.
Gishu
9
J'ai du mal à penser à des exemples où l'héritage aurait été ma réponse, je trouve souvent que l'agrégation, la composition et les interfaces donnent des solutions plus élégantes. Beaucoup des exemples ci-dessus pourraient être mieux expliqués en utilisant ces approches ...
Stuart Wakefield
415

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 .

Nick Zalutskiy
la source
107
Ce n'est pas toujours une approche parfaite, c'est simplement une bonne ligne directrice. le principe de substitution de Liskov est beaucoup plus précis (échoue moins).
Bill K
41
"Ma voiture a un véhicule." Si vous considérez cela comme une phrase distincte, pas dans un contexte de programmation, cela n'a absolument aucun sens. Et c'est tout l'intérêt de cette technique. Si cela semble gênant, c'est probablement faux.
Nick Zalutskiy
36
@Nick Bien sûr, mais "Ma voiture a un comportement de véhicule" est plus logique (je suppose que votre classe "Véhicule" pourrait être nommée "Véhicule"). Donc, vous ne pouvez pas baser votre décision sur "a un" vs "est une" comparaison, vous devez utiliser LSP, ou vous ferez des erreurs
Tristan
35
Au lieu de "est un" penser à "se comporte comme". L'héritage concerne l'héritage du comportement, pas seulement la sémantique.
ybakos
4
@ybakos "Se comporte comme" peut être réalisé via des interfaces sans avoir besoin d'héritage. De Wikipedia : "Une implémentation de la composition sur l'héritage commence généralement par la création de diverses interfaces représentant les comportements que le système doit présenter ... Ainsi, les comportements du système sont réalisés sans héritage."
DavidRR
210

Si vous comprenez la différence, c'est plus facile à expliquer.

Code de procédure

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.

Héritage

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.

Composition

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.

Composition sur héritage

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.

aleemb
la source
6
Pour l'héritage: il n'y a pas d'ambiguïté. Vous implémentez la classe Manager en fonction des exigences. Donc, vous renverriez "Manager of Operations" si c'est ce que vos exigences spécifiaient, sinon vous utiliseriez simplement l'implémentation de la classe de base. Vous pouvez également faire de Person une classe abstraite et ainsi vous assurer que les classes en aval implémentent une propriété Title.
Raj Rao
68
Il est important de se rappeler que l'on pourrait dire "Composition sur héritage" mais cela ne signifie pas "Composition toujours sur héritage". "Est un" signifie héritage et conduit à la réutilisation du code. L'employé est une personne (l'employé n'a pas de personne).
Raj Rao
36
L'exemple est déroutant: l'employé est une personne, il doit donc utiliser l'héritage. Vous ne devez pas utiliser la composition pour cet exemple, car il s'agit d'une mauvaise relation dans le modèle de domaine, même si techniquement vous pouvez la déclarer dans le code.
Michael Freidgeim
15
Je ne suis pas d'accord avec cet exemple. Un employé est une personne, qui est un cas classique d'utilisation appropriée de l'héritage. Je pense également que le «problème» de la redéfinition du champ Titre n'a pas de sens. Le fait que Employee.Title masque Person.Title est un signe de mauvaise programmation. Après tout, sont "M." et "Manager of Operations" se référant vraiment au même aspect d'une personne (minuscule)? Je renommerais Employee.Title, et ainsi être en mesure de référencer les attributs Title et JobTitle d'un employé, qui ont tous deux un sens dans la vie réelle. De plus, il n'y a aucune raison pour Manager (suite ...)
Radon Rosborough
9
(... suite) hériter de la personne et de l'employé - après tout, l'employé hérite déjà de la personne. Dans les modèles plus complexes, où une personne peut être un gestionnaire et un agent, il est vrai que l'héritage multiple peut être utilisé (avec précaution!), Mais il serait préférable dans de nombreux environnements d'avoir une classe de rôle abstraite à partir de laquelle le gestionnaire (contient des employés il / elle gère) et l'agent (contient les contrats et autres informations) héritent. Ensuite, un employé est une personne qui a plusieurs rôles. Ainsi, la composition et l'héritage sont utilisés correctement.
Radon Rosborough
142

Avec tous les avantages indéniables procurés par l'héritage, voici certains de ses inconvénients.

Inconvénients de l'héritage:

  1. Vous ne pouvez pas modifier l'implémentation héritée des super classes lors de l'exécution (évidemment parce que l'héritage est défini au moment de la compilation).
  2. L'héritage expose une sous-classe aux détails de son implémentation de classe parent, c'est pourquoi il est souvent dit que l'héritage rompt l'encapsulation (dans un sens que vous devez vraiment vous concentrer uniquement sur les interfaces et non sur l'implémentation, donc la réutilisation par sous-classe n'est pas toujours préférée).
  3. Le couplage étroit fourni par l'héritage rend l'implémentation d'une sous-classe très liée à l'implémentation d'une super classe que tout changement dans l'implémentation parent forcera la sous-classe à changer.
  4. Une réutilisation excessive par sous-classification peut rendre la pile d'héritage très profonde et très déroutante également.

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.

Galilyou
la source
5
C'est l'une des meilleures réponses, à mon avis - j'ajouterai à cela qu'essayer de repenser vos problèmes en termes de composition, selon mon expérience, tend à conduire à des classes plus petites, plus simples, plus autonomes, plus réutilisables , avec un champ de responsabilité plus clair, plus petit et plus ciblé. Souvent, cela signifie qu'il y a moins besoin de choses comme l'injection de dépendance ou la moquerie (dans les tests) car les composants plus petits sont généralement capables de se débrouiller seuls. Juste mon expérience. YMMV :-)
mindplay.dk
3
Le dernier paragraphe de ce post a vraiment cliqué pour moi. Je vous remercie.
Salx
87

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.

Tim Howland
la source
81

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.

Pavel Feldman
la source
5
Le bon outil pour le bon travail. Un marteau peut être plus efficace pour frapper des choses qu'une clé, mais cela ne signifie pas que l'on devrait considérer une clé comme "un marteau inférieur". L'héritage peut être utile lorsque les éléments ajoutés à la sous-classe sont nécessaires pour que l'objet se comporte comme un objet de superclasse. Par exemple, considérons une classe de base InternalCombustionEngineavec 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.
supercat
61

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

l'héritage est probablement la pire forme de couplage que vous puissiez avoir

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.

Mike Valenty
la source
L'héritage n'est ni bon ni mauvais, c'est simplement un cas particulier de composition. Où, en effet, la sous-classe met en œuvre une fonctionnalité similaire à la superclasse. Si la sous- classe que vous proposez ne réimplémente pas mais utilise simplement les fonctionnalités de la superclasse, vous avez mal utilisé l'héritage. C'est l'erreur du programmeur, pas une réflexion sur l'héritage.
iPherian
42

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.

dance2die
la source
10
+1 J'ai trouvé de moins en moins que l'héritage fonctionne dans la plupart des situations. Je préfère de loin les interfaces partagées / héritées et la composition des objets .... ou est-ce que cela s'appelle l'agrégation? Ne me demandez pas, j'ai un diplôme EE !!
kenny
Je crois que c'est le scénario le plus courant où la "composition sur héritage" s'applique, car les deux pourraient convenir en théorie. Par exemple, dans un système de marketing, vous pourriez avoir le concept d'un Client. Ensuite, un nouveau concept de PreferredClientpop-up apparaît plus tard. Devrait PreferredClienthé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, Accountpeut-être?
plalx
Plutôt que d'avoir différents types de Clientcours, il y en a peut-être un seul qui résume le concept d'un Accountqui pourrait être un StandardAccountou un PreferredAccount...
plalx
40

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

  1. 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.

  2. 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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 .

  5. La composition a l'avantage d' une programmation orientée combinateur , c'est-à-dire fonctionnant d'une manière similaire au modèle composite .

  6. La composition suit immédiatement la programmation vers une interface .

  7. 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.

lcn
la source
34

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.

Mecki
la source
"Beaucoup de gens pensent que l'héritage représente assez bien notre monde réel, mais ce n'est pas la vérité." Tant pis! Contrairement à presque tous les didacticiels de programmation dans le monde, la modélisation d'objets du monde réel en chaînes d'héritage est probablement une mauvaise idée à long terme. Vous ne devez utiliser l'héritage que lorsqu'il existe une relation incroyablement évidente, innée et simple. Comme TextFilec'est un File.
neonblitzer
25

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

Peter Tseng
la source
25

Pourquoi préférer la composition à l'héritage?

Voir d'autres réponses.

Quand pouvez-vous utiliser l'héritage?

On dit souvent qu'une classe Barpeut hériter d'une classe Foolorsque la phrase suivante est vraie:

  1. un bar est un foo

Malheureusement, le test ci-dessus n'est pas fiable à lui seul. Utilisez plutôt ce qui suit:

  1. un bar est un foo, ET
  2. les bars peuvent faire tout ce que les foos peuvent faire.

Le premier test garantit que tous les getters de Foosens dans Bar(= propriétés partagées), tandis que le second test s'assure que tous les setters de Foosens 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.

Quand devez-vous utiliser l'héritage?

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 Widgetdans les frameworks GUI ou une classe polymorphe Nodedans 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.

Principe de substitution de Liskov

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".

Boris Dalstein
la source
Les scénarios cercle-ellipse et carré-rectangle sont de mauvais exemples. Les sous-classes sont invariablement plus complexes que leur superclasse, donc le problème est artificiel. Ce problème est résolu en inversant la relation. Une ellipse dérive d'un cercle et un rectangle dérive d'un carré. Il est extrêmement stupide d'utiliser la composition dans ces scénarios.
Fuzzy Logic
@FuzzyLogic D'accord, mais en fait, mon message ne préconise jamais l'utilisation de la composition dans ce cas. J'ai seulement dit que le problème du cercle-ellipse est un excellent exemple de la raison pour laquelle "is-a" n'est pas un bon test à lui seul pour conclure que Circle devrait dériver d'Ellipse. Une fois que nous concluons qu'en fait, Circle ne devrait pas dériver d'Ellipse en raison de la violation de LSP, les options possibles sont d'inverser la relation, ou d'utiliser la composition, ou d'utiliser des classes de modèle, ou d'utiliser une conception plus complexe impliquant des classes supplémentaires ou des fonctions d'assistance, etc ... et la décision doit évidemment être prise au cas par cas.
Boris Dalstein
1
@FuzzyLogic Et si vous êtes curieux de savoir ce que je préconiserais pour le cas spécifique de Circle-Ellipse: je préconiserais de ne pas implémenter la classe Circle. Le problème avec l'inversion de la relation est qu'elle viole également LSP: imaginez la fonction 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.
Boris Dalstein
computeArea(Circle *c) { return pi * width * height / 4.0; }Maintenant c'est générique.
Fuzzy Logic
2
@FuzzyLogic Je ne suis pas d'accord: vous vous rendez compte que cela signifie que la classe Circle a anticipé l'existence de la classe dérivée Ellipse, et a donc fourni 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.
Boris Dalstein
19

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.

yukondude
la source
15

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).

egaga
la source
3
Il convient de noter que l'héritage n'est pas le seul moyen d'atteindre le polymorphisme. Le motif décorateur donne l'apparence du polymorphisme par la composition.
BitMask777
1
@ BitMask777: Le polymorphisme de sous-type n'est qu'un type de polymorphisme, un autre serait le polymorphisme paramétrique, vous n'avez pas besoin d'héritage pour cela. Plus important encore: quand on parle d'héritage, on veut dire héritage de classe; .ie vous pouvez avoir un polymorphisme de sous-type en ayant une interface commune pour plusieurs classes, et vous n'obtenez pas les problèmes d'héritage.
egaga
2
@engaga: J'ai interprété votre commentaire Inheritance is sometimes useful... That way you can have polymorphismcomme 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.
BitMask777
15

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 Engineexpose un mutateur pour changer ses
propriétés, soit je refonte Aircraftcomme:

Class Aircraft {
  var wings;
  var engine;
}

Maintenant, je peux aussi remplacer mon moteur à la volée.

simplfuzz
la source
Votre message soulève un point que je n'avais pas considéré auparavant - pour continuer votre analogie des objets mécaniques à plusieurs parties, sur quelque chose comme une arme à feu, il y a généralement une partie marquée d'un numéro de série, dont le numéro de série est considéré comme celle de l'arme à feu dans son ensemble (pour une arme de poing, ce serait généralement le cadre). On peut remplacer toutes les autres pièces et avoir toujours la même arme à feu, mais si le cadre se fissure et doit être remplacé, le résultat de l'assemblage d'un nouveau cadre avec toutes les autres pièces du pistolet d'origine serait un nouveau pistolet. Notez que ...
supercat
... le fait que plusieurs pièces d'une arme à feu puissent porter des numéros de série ne signifie pas qu'une arme à feu peut avoir plusieurs identités. Seul le numéro de série sur le châssis identifie le pistolet; le numéro de série sur toute autre pièce identifie le pistolet avec lequel ces pièces ont été fabriquées pour être assemblées, ce qui peut ne pas être le pistolet auquel elles sont assemblées à un moment donné.
supercat
7

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.

Anzurio
la source
7

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:

  1. Vous avez plusieurs classes avec la même interface et vous voulez gagner du temps en les écrivant
  2. Vous devez utiliser la même classe de base pour chaque objet
  3. Vous devez modifier les variables privées, qui ne peuvent en aucun cas être publiques

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.

Tero Tolonen
la source
7

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:

  • Nous découvrons qu'il y a d'autres fonctions que nous voulons que nos classes aient. Si nous ajoutons des fonctionnalités aux classes par héritage, nous devons décider - l'ajoutons-nous à la classe de base existante, même si toutes les classes qui en héritent n'ont pas besoin de cette fonctionnalité? Créons-nous une autre classe de base? Mais qu'en est-il des classes qui héritent déjà de l'autre classe de base?
  • Nous découvrons que pour une seule des classes qui hérite de notre classe de base, nous voulons que la classe de base se comporte un peu différemment. Alors maintenant, nous revenons en arrière et bricolons avec notre classe de base, peut-être en ajoutant des méthodes virtuelles, ou pire encore, du code qui dit: "Si je suis hérité de type A, faites ceci, mais si je hérite de type B, faites-le . " C'est mauvais pour beaucoup de raisons. La première est que chaque fois que nous changeons la classe de base, nous changeons effectivement chaque classe héritée. Nous changeons donc vraiment les classes A, B, C et D parce que nous avons besoin d'un comportement légèrement différent dans la classe A. Aussi prudent que nous le pensions, nous pourrions casser l'une de ces classes pour des raisons qui n'ont rien à voir avec celles Des classes.
  • Nous pouvons savoir pourquoi nous avons décidé de faire en sorte que toutes ces classes héritent les unes des autres, mais cela pourrait (probablement ne sera pas) logique pour quelqu'un d'autre qui doit maintenir notre code. Nous pourrions les forcer à faire un choix difficile - est-ce que je fais quelque chose de vraiment laid et désordonné pour faire le changement dont j'ai besoin (voir la puce précédente) ou est-ce que je réécris tout simplement un tas de ceci.

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.

Scott Hannen
la source
Le fait que les classes ne puissent pas utiliser plusieurs classes de base n'est pas une mauvaise réflexion sur l'héritage mais plutôt une mauvaise réflexion sur le manque de capacité d'un langage particulier.
iPherian
Dans le temps écoulé depuis la rédaction de cette réponse, j'ai lu ce post de "Oncle Bob" qui traite de ce manque de capacité. Je n'ai jamais utilisé de langage permettant l'héritage multiple. Mais en regardant en arrière, la question est étiquetée "agnostique du langage" et ma réponse suppose C #. J'ai besoin d'élargir mes horizons.
Scott Hannen
6

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.

Jon Limjap
la source
6

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.

Amir Aslam
la source
Vous avez dit héritage et héritage. ne voulez-vous pas dire héritage et composition?
trevorKirkby
Non, non. Vous pouvez tout aussi bien définir une interface canine et laisser chaque chien l'implémenter et vous vous retrouverez avec plus de code SOLIDE.
Markus
5

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).

YS
la source
+1 pour la mention de l'interface. J'utilise souvent cette approche pour masquer les classes existantes et rendre ma nouvelle classe correctement testable en se moquant de l'objet utilisé pour la composition. Cela nécessite que le propriétaire du nouvel objet lui transmette la classe parent candidate à la place.
Kell
4

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.

  • Votre classe fait-elle partie d'une structure qui profite du polymorphisme? Par exemple, si vous aviez une classe Shape, qui déclare une méthode appelée draw (), alors nous avons clairement besoin que les classes Circle et Square soient des sous-classes de Shape, afin que leurs classes clientes dépendent de Shape et non de sous-classes spécifiques.
  • Votre classe doit-elle réutiliser des interactions de haut niveau définies dans une autre classe? Le modèle de conception de méthode de modèle serait impossible à implémenter sans héritage. Je crois que tous les cadres extensibles utilisent ce modèle.

Cependant, si votre intention est purement celle de la réutilisation du code, alors la composition est probablement un meilleur choix de conception.

Parag
la source
4

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 ).

Enrique Molinari
la source
3

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é.


la source
2

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é.

Shami Qureshi
la source
2

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:

  1. 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

  2. Avec l'héritage, vous pouvez définir / modifier / étendre une capacité

    1. La classe de base ne fournit aucune implémentation et la sous-classe doit remplacer la méthode complète (abstraite) => Vous pouvez implémenter un contrat
    2. La classe de base fournit une implémentation par défaut et la sous-classe peut changer le comportement => Vous pouvez redéfinir le contrat
    3. La sous-classe ajoute une extension à l'implémentation de la classe de base en appelant super.methodName () comme première instruction => Vous pouvez étendre un contrat
    4. La classe de base définit la structure de l'algorithme et la sous-classe remplacera une partie de l'algorithme => Vous pouvez implémenter Template_method sans changer le squelette de la classe de base

Inconvénients de la composition:

  1. En héritage, la sous-classe peut appeler directement la méthode de classe de base même si elle n'implémente pas la méthode de classe de base en raison de la relation IS A. Si vous utilisez la composition, vous devez ajouter des méthodes dans la classe de conteneur pour exposer l'API de classe contenue

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();
     }
} 
Ravindra babu
la source
Je pense que cela ne répond pas à la question
almanegra
Vous pouvez revoir la question OP. J'ai abordé: Quels compromis y a-t-il pour chaque approche?
Ravindra babu
Comme vous l'avez mentionné, vous ne parlez que des "avantages de l'héritage et des inconvénients de la composition", et non des compromis pour CHAQUE approche ou des cas où vous devriez les utiliser les uns par rapport aux autres
almanegra
avantages et inconvénients fournit un compromis puisque les avantages de l'héritage sont les inconvénients de la composition et les inconvénients de la composition sont les avantages de l'héritage.
Ravindra babu
1

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

  1. Quelle conception de classe est la meilleure?
  2. Héritage vs agrégation
LCJ
la source
1
Je ne sais pas pourquoi «la classe de base est abstraite? chiffres dans la discussion .. le LSP: toutes les fonctions qui fonctionnent sur les chiens fonctionneront-elles si des objets Poodle sont passés? Si oui, alors Caniche peut remplacer Dog et peut donc hériter de Dog.
Gishu
@Gishu Merci. Je vais certainement me pencher sur LSP. Mais avant cela, pouvez-vous s'il vous plaît fournir un "exemple où l'héritage est approprié dans lequel la classe de base ne peut pas être abstraite". Ce que je pense, c'est que l'héritage n'est applicable que si la classe de base est abstraite. Si la classe de base doit être instanciée séparément, n'allez pas pour l'héritage. Autrement dit, même si le comptable est un employé, n'utilisez pas l'héritage.
LCJ
1
lu récemment WCF. Un exemple dans le framework .net est SynchronizationContext (la base + peut être instanciée) dont les files d'attente fonctionnent sur un thread ThreadPool. Les dérivations incluent WinFormsSyncContext (file d'attente sur le thread d'interface utilisateur) et DispatcherSyncContext (file d'attente sur WPF Dispatcher)
Gishu
@Gishu Merci. Cependant, il serait plus utile de fournir un scénario basé sur le domaine bancaire, le domaine RH, le domaine Retail ou tout autre domaine populaire.
LCJ
1
Désolé. Je ne connais pas ces domaines .. Un autre exemple si le précédent était trop obtus est la classe Control dans Winforms / WPF. Le contrôle de base / générique peut être instancié. Les dérivations incluent les zones de liste, les zones de texte, etc. Maintenant que j'y pense, le modèle de conception de décorateur est un bon exemple à mon humble avis et utile aussi. Le décorateur dérive de l'objet non abstrait qu'il veut envelopper / décorer.
Gishu
1

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.

Veverke
la source
1
D hérite de B et C pas A. Si c'est le cas, alors il utiliserait l'implémentation de X qui est en classe A.
fabricio
1
@fabricio: merci, j'ai édité le texte. Soit dit en passant, un tel scénario ne peut pas se produire dans des langages qui ne permettent pas l'héritage de plusieurs classes, ai-je raison?
Veverke
oui vous avez raison .. et je n'ai jamais travaillé avec celui qui permet l'héritage multiple (selon l'exemple du problème diamant) ..
fabricio