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:
Héritage: étendre 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. Faites en sorte que les méthodes abstract/virtual forcent les sous-classes à "remplir-les-blancs" lorsque la super-classe souhaite une interface particulière mais est agnostique quant à sa mise en oeuvre.
Agrégation: créez de nouvelles fonctionnalités en prenant d'autres classes et en les combinant dans une nouvelle classe. Associez une interface commune à cette nouvelle classe pour une interopérabilité avec un autre code.
Quels sont les avantages, les coûts et les conséquences de chacun? Y a-t-il d'autres alternatives?
Je vois que ce débat revient régulièrement, mais je ne pense pas qu'il ait été demandé pour Stack Overflow (bien qu'il y ait une discussion connexe). Il y a aussi un manque surprenant de bons résultats Google pour cela.
Ce n’est pas une question de savoir qui est le meilleur, mais de savoir quand utiliser quoi.
Dans les cas "normaux", une simple question suffit à déterminer si nous avons besoin d'héritage ou d'agrégation.
Cependant, il y a une grande zone grise. Nous avons donc besoin de plusieurs autres astuces.
Pour le 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 modifications majeures. Et en cas de doute, utilisez Agrégation.
Une autre possibilité, dans le cas où nous aurions une classe nécessitant une partie des fonctionnalités de la classe d'origine, consiste à scinder 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 devriez faire attention à cela, ne pas créer une séparation illogique.
Ajoutons un exemple. Nous avons une classe 'Chien' avec des méthodes: 'manger', 'marcher', 'écorce', 'jouer'.
class Dog
Eat;
Walk;
Bark;
Play;
end;
Nous avons maintenant besoin d'une classe "Chat", qui a besoin de "Manger", "Marcher", "Ronronner" et "Jouer". Alors essayez d'abord de l'étendre d'un chien.
class Cat is Dog
Purr;
end;
Regarde, d'accord, mais attends. 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 redéfinir la méthode Bark pour qu’elle ne fasse rien.
class Cat is Dog
Purr;
Bark = null;
end;
Ok, ça marche, mais ça sent mauvais. Essayons donc 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 de compagnie pour étendre le modèle. Sauf si c'est un poisson, ou quelque chose qui ne marche pas. Dans ce cas, nous devons encore une fois refactoriser. Mais c'est quelque chose pour une autre fois.
La différence est généralement exprimée par la différence entre "est un" et "a". L'héritage, la relation "est un", 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 l'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).
Voici mon argument le plus commun:
Dans tout système orienté objet, chaque classe est divisée en deux parties:
Son interface: la "face publique" de l'objet. C'est l'ensemble des capacités qu'il annonce au reste du monde. Dans beaucoup de langues, l'ensemble est bien défini dans une "classe". Généralement, ce sont les signatures de méthode de l'objet, bien que cela varie un peu en fonction du langage.
Son implémentation: le travail "dans les coulisses" que fait l'objet pour satisfaire son interface et fournir 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 OOP) est que l'implémentation est encapsulée (c'est-à-dire: cachée) au sein de la classe; la seule chose que les utilisateurs externes doivent voir est l'interface.
Lorsqu'une sous-classe hérite d'une sous-classe, elle hérite généralement de les deux l'implémentation et l'interface. Ceci, à son tour, signifie que vous êtes forcé d'accepter les deux comme contraintes pour votre classe.
Avec l'agrégation, vous pouvez choisir l'implémentation ou l'interface, ou les deux, mais vous n'y êtes pas obligé. La fonctionnalité d'un objet est laissée à l'objet lui-même. Il peut s'en remettre à d'autres objets à sa guise, 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.
J'ai donné une réponse à "Est-ce qu'un" est "a un": lequel est le meilleur? .
Fondamentalement, je suis d’accord avec d’autres personnes: utilisez l’héritage uniquement si votre classe dérivée est vraiment le type que vous étendez, pas simplement parce que contient les mêmes données. Rappelez-vous que l'héritage signifie que la sous-classe gagne les méthodes ainsi que les données.
Est-il judicieux que votre classe dérivée ait toutes les méthodes de la super-classe? Ou bien vous promettez-vous discrètement que ces méthodes devraient être ignorées dans la classe dérivée? Ou trouvez-vous que vous passez outre les méthodes de la super-classe, les rendant ainsi inutiles, afin que personne ne les appelle par inadvertance? Ou donner des conseils à votre outil de génération de documentation API pour omettre la méthode de la documentation?
Ce sont des indices forts que l'agrégation est le meilleur choix dans ce cas.
Je vois beaucoup de réponses "est-a vs a-a; elles sont conceptuellement différentes" sur cette question et les questions connexes.
Mon expérience m'a montré que tenter de déterminer si une relation est "est-un" ou "a-un" est voué à l'échec. Même si vous pouvez maintenant déterminer correctement les objets, l'évolution des exigences signifie que vous vous tromperez probablement à l'avenir.
Une autre chose que j'ai trouvée est qu'il est très difficile de convertir un héritage en agrégation une fois que beaucoup de code est écrit autour d'une hiérarchie d'héritage. Passer d'une super-classe à une interface signifie changer presque toutes les sous-classes du système.
Et, comme je l'ai mentionné ailleurs dans ce billet, l'agrégation a tendance à être moins flexible que l'héritage.
Donc, vous avez une tempête parfaite d'arguments contre l'héritage chaque fois que vous devez choisir l'un ou l'autre:
Ainsi, j’ai tendance à choisir l’agrégation - même s’il semble y avoir une relation forte est-une.
Je voulais en faire un commentaire sur la question initiale, mais 300 caractères mordent [; <).
Je pense que nous devons faire attention. Premièrement, il y a plus de saveurs que les deux exemples plutôt spécifiques cités dans la question.
De plus, je suggère qu'il soit utile de ne pas confondre l'objectif avec l'instrument. On veut s'assurer que la technique ou la méthodologie choisie contribue à la réalisation de l'objectif principal, mais je ne pense pas qu'il soit très utile de discuter hors-contexte quelle technique est la meilleure solution. Il est utile de connaître les pièges des différentes approches avec leurs points forts clairs.
Par exemple, que voulez-vous accomplir, de quoi disposez-vous pour commencer et quelles sont les contraintes?
Créez-vous une structure de composant, même à vocation spécifique? Les interfaces sont-elles séparables des implémentations dans le système de programmation ou sont-elles réalisées par une pratique utilisant un type de technologie différent? 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 cacher la structure de classe d'une implémentation au code qui repose sur les interfaces fournies par l'implémentation? Existe-t-il plusieurs implémentations pouvant être utilisables en même temps ou la variation est-elle plus progressive dans le temps en raison de la maintenance et de l'amélioration? Ceci et plus doit être pris en compte avant de vous concentrer sur un outil ou une méthodologie.
Enfin, est-il si important de verrouiller les distinctions dans l’abstraction et comment vous en pensez (comme dans is-a versus has-a) à différentes caractéristiques de la OO technologie? Peut-être, 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 créer. Peut-être est-il préférable de prendre du recul et de ne pas être aussi rigide ( la narration pour que d’autres puissent dire ce qui se passe). [Je cherche ce qui rend une partie particulière d’un programme explicable, mais parfois, je recherche l’élégance quand il ya 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é, que ce soit pour la construction d’un framework Java) ou l’organisation de certaines implémentations COM. approprié pour tout, même pas près de tout, même si je le jure (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.)
La question est normalement formulée comme Composition vs. Héritage , et elle a été posée ici auparavant.
Je pense que ce n'est pas un débat ni/ou. C'est juste ça:
Les deux ont leur place, mais l'héritage est plus risqué.
Bien sûr, cela n’aurait aucun 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.
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/flexibilité que de généralisation. Cette extension/flexibilité fait principalement référence à la flexibilité d'exécution/dynamique.
L'avantage n'est pas visible immédiatement. Pour voir les avantages, vous devez attendre la prochaine demande de modification inattendue. Ainsi, dans la plupart des cas, ceux qui sont attachés à la généralisation échouent par rapport à ceux qui ont adopté la composition (sauf un cas évident mentionné plus tard). D'où la règle. D'un point de vue d'apprentissage, si vous pouvez implémenter une injection de dépendance avec succès, vous devez savoir à qui et à quel moment préférer. La règle vous aide également à prendre une décision. si vous n’êtes pas sûr, choisissez la composition.
Résumé: Composition: le couplage est réduit simplement en ayant quelques petites choses que vous branchez dans quelque chose de plus grand, et le plus gros objet rappelle simplement le plus petit objet. Généralisation: du point de vue de l'API, définir qu'une méthode peut être remplacée est un engagement plus important que de définir qu'une méthode peut être appelée. (très peu d'occasions quand la généralisation gagne). Et n'oubliez jamais qu'avec la composition, vous utilisez également l'héritage, à partir d'une interface plutôt que d'une grande classe
Je couvrirai la partie où-ces-pourraient-appliquer. Voici un exemple des deux, dans un scénario de jeu. Supposons qu'il y ait un jeu qui comporte différents types de soldats. Chaque soldat peut avoir un sac à dos pouvant contenir différentes choses.
Héritage ici? Il y a un béret marin, vert et un tireur d'élite. Ce sont des types de soldats. Donc, il y a un soldat de classe de base avec Marine, Green Beret & Sniper comme classes dérivées
Agrégation ici? Le sac à dos peut contenir des grenades, des armes à feu (différents types), un couteau, un medikit, etc. Un soldat peut en être équipé à tout moment, mais il peut aussi avoir gilet pare-balles qui fait office d’armure lorsqu’il est attaqué et dont la blessure diminue à un certain pourcentage. La classe soldat contient un objet de la classe gilet pare-balles et la classe knapsack qui contient des références à ces articles.
Les deux approches sont utilisées pour résoudre différents problèmes. Vous n'avez pas toujours besoin d'agréger deux ou plusieurs classes pour hériter d'une classe.
Parfois, vous devez agréger une seule classe car cette classe est scellée ou doit être interceptée par des membres non virtuels. Vous devez donc créer une couche proxy qui n'est évidemment pas valide en termes d'héritage, mais tant que la classe que vous utilisez comme serveur proxy a une interface à laquelle vous pouvez vous inscrire, cela peut assez bien fonctionner.