Héritage vs agrégation [fermé]

151

Il existe deux écoles de pensée sur la meilleure façon d'étendre, d'améliorer et de réutiliser le code dans un système orienté objet:

  1. Héritage: étendez les fonctionnalités d'une classe en créant une sous-classe. Remplacez les membres de la superclasse dans les sous-classes pour fournir de nouvelles fonctionnalités. Rendre les méthodes abstraites / virtuelles pour forcer les sous-classes à "remplir les blancs" lorsque la superclasse veut une interface particulière mais est indépendante de son implémentation.

  2. Agrégation: créez de nouvelles fonctionnalités en prenant d'autres classes et en les combinant dans une nouvelle classe. Attachez une interface commune à cette nouvelle classe pour l'interopérabilité avec d'autres codes.

Quels sont les avantages, les coûts et les conséquences de chacun? Existe-t-il d'autres alternatives?

Je vois ce débat venir régulièrement, mais je ne pense pas qu'il ait encore été demandé sur Stack Overflow (bien qu'il y ait une discussion connexe). Il y a aussi un manque surprenant de bons résultats Google pour cela.

Craig Walker
la source
9
Belle question, malheureusement je n'ai pas assez de temps maintenant.
Toon Krijthe
4
Une bonne réponse vaut mieux qu'une réponse plus rapide ... Je vais regarder ma propre question donc je vous voterai au moins :-P
Craig Walker

Réponses:

181

Il ne s'agit pas de savoir lequel est le meilleur, mais de savoir quand utiliser quoi.

Dans les cas «normaux», une simple question suffit pour savoir si nous avons besoin d'héritage ou d'agrégation.

  • Si La nouvelle classe est plus ou moins la classe d'origine. Utilisez l'héritage. La nouvelle classe est désormais une sous-classe de la classe d'origine.
  • Si la nouvelle classe doit avoir la classe d'origine. Utilisez l'agrégation. La nouvelle classe a maintenant la classe d'origine en tant que membre.

Cependant, il y a une grande zone grise. Nous avons donc besoin de plusieurs autres astuces.

  • Si nous avons utilisé l'héritage (ou si nous prévoyons de l'utiliser) mais que nous n'utilisons qu'une partie de l'interface, ou nous sommes obligés de remplacer beaucoup de fonctionnalités pour garder la corrélation logique. Ensuite, nous avons une grosse odeur désagréable qui indique que nous avons dû utiliser l'agrégation.
  • Si nous avons utilisé l'agrégation (ou si nous prévoyons de l'utiliser), mais nous découvrons que nous devons copier presque toutes les fonctionnalités. Ensuite, nous avons une odeur qui pointe dans la direction de l'héritage.

Pour couper court. Nous devrions utiliser l'agrégation si une partie de l'interface n'est pas utilisée ou doit être modifiée pour éviter une situation illogique. Nous n'avons besoin d'utiliser l'héritage que si nous avons besoin de presque toutes les fonctionnalités sans changements majeurs. Et en cas de doute, utilisez l'agrégation.

Une autre possibilité pour, le cas où nous avons une classe qui a besoin d'une partie des fonctionnalités de la classe d'origine, est de diviser la classe d'origine en une classe racine et une sous-classe. Et laissez la nouvelle classe hériter de la classe racine. Mais vous devez faire attention à cela, ne pas créer une séparation illogique.

Ajoutons un exemple. Nous avons une classe «Chien» avec des méthodes: «Eat», «Walk», «Bark», «Play».

class Dog
  Eat;
  Walk;
  Bark;
  Play;
end;

Nous avons maintenant besoin d'une classe «Chat», qui a besoin de «Mange», «Marche», «Ronronne» et «Joue». Alors essayez d'abord de l'étendre à partir d'un chien.

class Cat is Dog
  Purr; 
end;

Regarde, d'accord, mais attendez. Ce chat peut aboyer (les amoureux des chats me tueront pour ça). Et un chat qui aboie viole les principes de l'univers. Nous devons donc remplacer la méthode Bark afin qu'elle ne fasse rien.

class Cat is Dog
  Purr; 
  Bark = null;
end;

Ok, ça marche, mais ça sent mauvais. Alors essayons une agrégation:

class Cat
  has Dog;
  Eat = Dog.Eat;
  Walk = Dog.Walk;
  Play = Dog.Play;
  Purr;
end;

Ok, c'est bien. Ce chat n'aboie plus, pas même silencieux. Mais il a toujours un chien interne qui veut sortir. Essayons donc la solution numéro trois:

class Pet
  Eat;
  Walk;
  Play;
end;

class Dog is Pet
  Bark;
end;

class Cat is Pet
  Purr;
end;

C'est beaucoup plus propre. Pas de chiens internes. Et les chats et les chiens sont au même niveau. Nous pouvons même introduire d'autres animaux pour étendre le modèle. À moins que ce ne soit un poisson ou quelque chose qui ne marche pas. Dans ce cas, nous devons à nouveau refactoriser. Mais c'est quelque chose pour une autre fois.

Toon Krijthe
la source
5
La "réutilisation de presque toutes les fonctionnalités d'une classe" est la seule fois où je préfère l'héritage. Ce que j'aimerais vraiment, c'est un langage qui a la capacité de dire facilement "déléguer à cet objet agrégé pour ces méthodes spécifiques"; c'est le meilleur des deux mondes.
Craig Walker
Je ne suis pas sûr des autres langages, mais Delphi a un mécanisme qui permet aux membres d'implémenter une partie de l'interface.
Toon Krijthe
2
Objective C utilise des protocoles qui vous permettent de décider si votre fonction est obligatoire ou facultative.
cantfindaname88
1
Excellente réponse avec un exemple pratique. Je créerais la classe Pet avec une méthode appelée makeSoundet laisserais chaque sous-classe implémenter son propre type de son. Cela vous aidera ensuite dans les situations où vous avez beaucoup d'animaux de compagnie et que vous le faites for_each pet in the list of pets, pet.makeSound.
blongho
38

Au début de GOF, ils déclarent

Privilégiez la composition d'objets par rapport à l'héritage de classe.

Ceci est discuté plus en détail ici

boîte à outils
la source
27

La différence est généralement exprimée comme la différence entre «est un» et «a un». L'héritage, la relation «est une», se résume bien dans le principe de substitution de Liskov . L'agrégation, la relation «a une», n'est que cela - cela montre que l'objet d'agrégation a un des objets agrégés.

D'autres distinctions existent également - l'héritage privé en C ++ indique une relation «est implémentée en termes de», qui peut également être modélisée par l'agrégation d'objets membres (non exposés).

Harper Shelby
la source
En outre, la différence se résume bien dans la question même à laquelle vous êtes censé répondre. J'aimerais que les gens lisent d'abord les questions avant de commencer à traduire. Faites-vous la promotion aveugle des modèles de design pour devenir membre du club d'élite?
Val
Je n'aime pas cette approche, c'est plus une question de composition
Alireza Rahmani Khalili
15

Voici mon argument le plus courant:

Dans tout système orienté objet, il y a deux parties à toute classe:

  1. Son interface : le "visage public" de l'objet. C'est l'ensemble des capacités qu'il annonce au reste du monde. Dans de nombreux langages, l'ensemble est bien défini dans une "classe". Ce sont généralement les signatures de méthode de l'objet, bien que cela varie un peu selon la langue.

  2. Sa mise en œuvre : le travail «en coulisse» que fait l'objet pour satisfaire son interface et lui apporter des fonctionnalités. Il s'agit généralement du code et des données de membre de l'objet.

L'un des principes fondamentaux de la POO est que l'implémentation est encapsulée (c'est-à-dire cachée) dans la classe; la seule chose que les étrangers devraient voir est l'interface.

Lorsqu'une sous-classe hérite d'une sous-classe, elle hérite généralement à la fois de l'implémentation et de l'interface. Cela, à son tour, signifie que vous êtes forcé d'accepter les deux comme des contraintes pour votre classe.

Avec l'agrégation, vous pouvez choisir l'implémentation ou l'interface, ou les deux, mais vous n'êtes pas obligé de le faire non plus. La fonctionnalité d'un objet est laissée à l'objet lui-même. Il peut s'en remettre à d'autres objets comme il l'entend, mais il est finalement responsable de lui-même. D'après mon expérience, cela conduit à un système plus flexible: un système plus facile à modifier.

Ainsi, chaque fois que je développe un logiciel orienté objet, je préfère presque toujours l'agrégation à l'héritage.

Craig Walker
la source
Je pense que vous utilisez une classe abstraite pour définir une interface, puis que vous utilisez l'héritage direct pour chaque classe d'implémentation concrète (ce qui la rend assez superficielle). Toute agrégation est sous la mise en œuvre concrète.
orcmid
Si vous avez besoin d'interfaces supplémentaires, retournez-les via des méthodes sur l'interface de l'interface abstraite de niveau principal, rincez à nouveau.
orcmid
Pensez-vous que l'agrégation est meilleure dans ce scénario? Un livre est un SellingItem, un DigitalDisc est un SelliingItem codereview.stackexchange.com/questions/14077/…
LCJ
11

J'ai donné une réponse à "Est-ce qu'un" vs "A un": lequel est le meilleur? .

Fondamentalement, je suis d'accord avec les autres: n'utilisez l'héritage que si votre classe dérivée vraiment est le type que vous étendez, pas simplement parce qu'elle contient les mêmes données. N'oubliez pas que l'héritage signifie que la sous-classe obtient les méthodes ainsi que les données.

Est-il logique que votre classe dérivée ait toutes les méthodes de la superclasse? Ou vous promettez-vous tranquillement que ces méthodes doivent être ignorées dans la classe dérivée? Ou vous trouvez-vous en train de remplacer les méthodes de la superclasse, ce qui en fait des non-opérations pour que personne ne les appelle par inadvertance? Ou donner des conseils à votre outil de génération de doc API pour omettre la méthode du document?

Ce sont des indices forts que l'agrégation est le meilleur choix dans ce cas.

Bill Karwin
la source
6

Je vois beaucoup de réponses «is-a vs has-a; ils sont conceptuellement différents» sur cette question et les questions connexes.

La seule chose que j'ai trouvée dans mon expérience est qu'essayer de déterminer si une relation est «est-un» ou «a-a» est voué à l'échec. Même si vous pouvez effectuer correctement cette détermination pour les objets maintenant, les exigences changeantes signifient que vous vous tromperez probablement à un moment donné dans le futur.

Une autre chose que j'ai trouvée est que c'est très difficile de de l'héritage à l'agrégation une fois qu'il y a beaucoup de code écrit autour d'une hiérarchie d'héritage. Le simple passage d'une superclasse à une interface signifie changer presque toutes les sous-classes du système.

Et, comme je l'ai mentionné ailleurs dans cet article, l'agrégation a tendance à être moins flexible que l'héritage.

Ainsi, vous avez une tempête parfaite d'arguments contre l'héritage chaque fois que vous devez choisir l'un ou l'autre:

  1. Votre choix sera probablement le mauvais à un moment donné
  2. Changer ce choix est difficile une fois que vous l'avez fait.
  3. L'héritage a tendance à être un pire choix car il est plus contraignant.

Ainsi, j'ai tendance à choisir l'agrégation - même lorsqu'il semble y avoir une forte relation is-a.

Craig Walker
la source
L'évolution des exigences signifie que votre conception OO sera inévitablement erronée. L'héritage contre l'agrégation n'est que la pointe de cet iceberg. Vous ne pouvez pas concevoir tous les futurs possibles, alors allez-y simplement avec l'idée XP: résolvez les exigences d'aujourd'hui et acceptez que vous deviez peut-être refactoriser.
Bill Karwin
J'aime cette piste d'enquête et de questionnement. Préoccupé par le blurrung est-un, a-un, et utilise-un bien. Je commence par les interfaces (avec l'identification des abstractions implémentées que je souhaite utiliser). Le niveau supplémentaire d'indirection est inestimable. Ne suivez pas votre conclusion d'agrégation.
orcmid
C'est à peu près mon point cependant. L'héritage et l'agrégation sont des méthodes pour résoudre le problème. L'une de ces méthodes entraîne toutes sortes de sanctions lorsque vous devez résoudre des problèmes demain.
Craig Walker
Dans mon esprit, l'agrégation + les interfaces sont l'alternative à l'héritage / sous-classement; vous ne pouvez pas faire de polymorphisme basé uniquement sur l'agrégation.
Craig Walker
3

La question est normalement formulée comme Composition vs Héritage , et elle a déjà été posée ici.

Bill le lézard
la source
Vous avez raison ... drôle que SO n'ait pas donné celle-là comme l'une des questions connexes.
Craig Walker
2
Alors la recherche craint toujours beaucoup
Vinko Vrsalovic
D'accord. C'est pourquoi je n'ai pas fermé ça. Cela, et beaucoup de gens avaient déjà répondu ici.
Bill the Lizard
1
SO a besoin d'une fonctionnalité pour rouler 2 questions (et leurs réponses) en 1
Craig Walker
3

Je voulais faire un commentaire sur la question originale, mais 300 caractères mordent [; <).

Je pense que nous devons être prudents. Premièrement, il y a plus de saveurs que les deux exemples assez spécifiques faits dans la question.

De plus, je suggère qu'il est utile de ne pas confondre l'objectif avec l'instrument. On veut s'assurer que la technique ou la méthodologie choisie soutient la réalisation de l'objectif principal, mais je ne pense pas que la discussion hors contexte sur quelle technique est la meilleure soit très utile. Il est utile de connaître les pièges des différentes approches ainsi que leurs points forts clairs.

Par exemple, que comptez-vous accomplir, de quoi disposez-vous pour commencer et quelles sont les contraintes?

Créez-vous un framework de composants, même spécifique? Les interfaces sont-elles séparables des implémentations dans le système de programmation ou est-ce réalisé par une pratique utilisant un autre type de technologie? Pouvez-vous séparer la structure d'héritage des interfaces (le cas échéant) de la structure d'héritage des classes qui les implémentent? Est-il important de masquer la structure de classe d'une implémentation du code qui repose sur les interfaces fournies par l'implémentation? Y a-t-il plusieurs implémentations utilisables en même temps ou la variation est-elle plus au fil du temps en raison de la maintenance et de l'amélioration? Ceci et bien d'autres doivent être pris en compte avant de vous fixer sur un outil ou une méthodologie.

Enfin, est-il si important de verrouiller les distinctions dans l'abstraction et comment vous y pensez (comme dans is-a versus has-a) aux différentes caractéristiques de la technologie OO? Peut-être que oui, si cela maintient la structure conceptuelle cohérente et gérable pour vous et les autres. Mais il est sage de ne pas être asservi par cela et les contorsions que vous pourriez finir par faire. Il est peut-être préférable de prendre du recul et de ne pas être aussi rigide (mais laissez une bonne narration pour que les autres puissent dire ce qui se passe). [Je cherche ce qui rend une partie particulière d'un programme explicable, mais parfois je privilégie l'élégance quand il y a une plus grande victoire. Ce n’est pas toujours la meilleure idée.]

Je suis un puriste d'interface et je suis attiré par les types de problèmes et d'approches où le purisme d'interface est approprié, qu'il s'agisse de créer un cadre Java ou d'organiser certaines implémentations COM. Cela ne le rend pas approprié pour tout, pas même près de tout, même si je ne jure que par cela. (J'ai quelques projets qui semblent fournir de sérieux contre-exemples contre le purisme d'interface, il sera donc intéressant de voir comment j'arrive à faire face.)

orcmid
la source
2

Je couvrirai la partie où-ces-pourraient-s'appliquer. Voici un exemple des deux, dans un scénario de jeu. Supposons qu'il existe un jeu qui a différents types de soldats. Chaque soldat peut avoir un sac à dos qui peut contenir différentes choses.

L'héritage ici? Il y a un béret vert marin et un tireur d'élite. Ce sont des types de soldats. Donc, il y a un soldat de classe de base avec Marine, Béret vert et Sniper comme classes dérivées

Agrégation ici? Le sac à dos peut contenir des grenades, des fusils (différents types), un couteau, un medikit, etc. Un soldat peut être équipé de n'importe lequel de ces éléments à tout moment, et il peut également avoir un gilet pare-balles qui agit comme une armure lorsqu'il les blessures diminuent jusqu'à un certain pourcentage. La classe de soldat contient un objet de classe de gilet pare-balles et la classe de sac à dos qui contient des références à ces éléments.

Salman Kasbati
la source
2

Je pense que ce n'est pas un débat entre les deux. C'est juste ça:

  1. Les relations is-a (héritage) se produisent moins souvent que les relations has-a (composition).
  2. L'héritage est plus difficile à obtenir, même lorsqu'il est approprié de l'utiliser, il faut donc faire preuve de diligence raisonnable car il peut briser l'encapsulation, encourager un couplage étroit en exposant la mise en œuvre, etc.

Les deux ont leur place, mais l'héritage est plus risqué.

Bien sûr, cela n'aurait pas de sens d'avoir une classe Shape 'ayant-un' Point et une classe Square. Ici l'héritage est dû.

Les gens ont tendance à penser d'abord à l'héritage lorsqu'ils essaient de concevoir quelque chose d'extensible, c'est ce qui ne va pas.

Vinko Vrsalovic
la source
1

La faveur se produit lorsque les deux candidats se qualifient. A et B sont des options et vous préférez A. La raison en est que la composition offre plus de possibilités d'extension / flexiblité que la généralisation. Cette extension / flexibilité se réfère principalement à la flexibilité d'exécution / dynamique.

L'avantage n'est pas immédiatement visible. Pour voir l'avantage, vous devez attendre la prochaine demande de modification inattendue. Ainsi, dans la plupart des cas, ceux qui sont restés fidèles à la généralisation échouent par rapport à ceux qui ont adopté la composition (sauf un cas évident mentionné plus loin). D'où la règle. D'un point de vue d'apprentissage, si vous pouvez implémenter une injection de dépendances avec succès, vous devez savoir lequel privilégier et quand. La règle vous aide également à prendre une décision; si vous n'êtes pas sûr, sélectionnez la composition.

Résumé: Composition: Le couplage est réduit en ayant simplement des choses plus petites que vous branchez à quelque chose de plus grand, et le plus gros objet rappelle simplement le plus petit objet. Génération: du point de vue de l'API, définir qu'une méthode peut être remplacée est un engagement plus fort que de définir qu'une méthode peut être appelée. (très peu d'occasions lorsque la généralisation l'emporte). Et n'oubliez jamais qu'avec la composition, vous utilisez aussi l'héritage, à partir d'une interface au lieu d'une grande classe

Nuages ​​bleus
la source
0

Les deux approches sont utilisées pour résoudre différents problèmes. Vous n'avez pas toujours besoin d'agréger plus de deux classes ou plus lors de l'héritage d'une classe.

Parfois, vous devez agréger une seule classe parce que cette classe est scellée ou a des membres non virtuels que vous devez intercepter afin de créer une couche proxy qui n'est évidemment pas valide en termes d'héritage, mais tant que la classe que vous utilisez par proxy a une interface à laquelle vous pouvez vous abonner, cela peut fonctionner assez bien.

cfeduke
la source