Héritage ou composition: comptez-vous sur “est-a” et “a-a”?

Lorsque je conçois des classes et que je dois choisir entre l’inheritance et la composition, j’utilise généralement la règle générale: si la relation est “est-un” – utilise l’inheritance et si la relation est “a-un” – utilise la composition.

Est-ce toujours vrai?

Je vous remercie.

Non – “est un” ne conduit pas toujours à l’inheritance. Un exemple bien cité est la relation entre un carré et un rectangle. Un carré est un rectangle, mais il serait mauvais de concevoir un code héritant d’une classe Square d’une classe Rectangle.

Ma suggestion est d’améliorer votre heuristique avec le principe de substitution de Liskov . Pour vérifier si une relation d’inheritance est conforme au principe de substitution de Liskov, demandez si les clients d’une classe de base peuvent opérer sur la sous-classe sans savoir qu’elle opère sur une sous-classe. Bien entendu, toutes les propriétés de la sous-classe doivent être préservées.

Dans l’exemple carré / rectangle, il faut se demander si un client de rectangle peut opérer sur un carré sans savoir qu’il s’agit d’un carré. Tout ce que le client doit savoir, c’est qu’il opère sur un rectangle. La fonction suivante illustre un client qui suppose que définir la largeur d’un rectangle laisse la hauteur inchangée.

void g(Rectangle& r) { r.SetWidth(5); r.SetHeight(4); assert(r.GetWidth() * r.GetHeight()) == 20); } 

Cette hypothèse est vraie pour un rectangle, mais pas pour un carré. Donc, la fonction ne peut pas fonctionner sur un carré et par conséquent, la relation d’inheritance viole le principe de substitution de Liskov. (D’ailleurs, l’exemple vient de l’ article lié .)

Oui et non.

La ligne peut être floue. Cela n’a pas été aidé par des exemples assez terribles de programmation OO depuis les débuts de OO, comme par exemple: Manager est un employé est une personne.

La chose à retenir à propos de l’inheritance est que: l’inheritance rompt l’encapsulation. L’inheritance est un détail d’implémentation. Il y a toutes sortes d’écrits sur ce sujet.

Le moyen le plus simple de le résumer est:

Préfère la composition.

Cela ne veut pas dire l’utiliser à l’exclusion complète de l’inheritance. Cela signifie simplement que l’inheritance est une position de repli.

Si la relation est “est-a” – utilisez l’inheritance, et si la relation est “a-a” – utilisez la composition. Est-ce toujours vrai?

Dans un sens, oui. Mais vous devez faire attention à ne pas introduire des relations inutiles et artificielles “is-a”.

Par exemple, on peut penser qu’un ThickBorderedRectangle est un rectangle, ce qui semble raisonnable à première vue, et décider d’utiliser l’inheritance. Mais on décrit mieux cette situation en disant qu’un rectangle a une bordure, qui peut être ou non un ThickBorder. Dans ce cas, il préférerait mieux la composition.

De la même manière, on peut penser qu’un ThickBorder est une frontière spéciale et utiliser l’inheritance; mais il vaut mieux dire qu’une bordure a une largeur, préférant donc la composition.

Dans tous ces cas ambigus, mes règles empiriques sont mûrement réfléchies et, comme d’autres l’ont conseillé, préfèrent la composition à la succession .

Je ne pense pas que ce soit aussi simple que “est-un” vs “a-un” – comme l’a dit Cletus, la ligne peut être très floue. Ce n’est pas comme si deux personnes en arrivaient toujours à la même conclusion, étant donné que c’était à titre indicatif.

Aussi, comme le dit Cletus, préférez la composition à l’inheritance. À mon sens, il doit exister de très bonnes raisons de dériver d’une classe de base – et en particulier, la classe de base doit avoir été conçue pour l’inheritance et pour le type de spécialisation que vous souhaitez appliquer.

Au-delà de cela – et ce sera probablement une idée impopulaire – il s’agit de goûter et de sentir. Au fil du temps, je pense que les bons développeurs ont une meilleure idée du moment où l’inheritance fonctionnera ou non, mais ils peuvent avoir du mal à exprimer clairement. Je sais que je trouve cela difficile, comme le montre cette réponse. Peut-être suis-je simplement en train de projeter la difficulté sur d’autres personnes 🙂

L’inheritance ne signifie pas toujours qu’il existe une relation “est-une” et l’absence d’une relation ne signifie pas toujours qu’il n’y en a pas.

Exemples:

  • Si vous utilisez un inheritance protégé ou privé, vous perdez la substituabilité externe, c’est-à-dire que toute personne en dehors de votre hiérarchie est concernée, une dérivée n’est pas un type de base.
  • Si vous substituez un membre virtuel de base et modifiez le comportement observable d’une personne référençant la base, de l’implémentation de base d’origine, vous avez effectivement rompu la substituabilité. Même si vous avez un inheritance public, la hiérarchie dérivée n’est pas une base.
  • Votre classe peut parfois être substituable à une autre sans aucune relation d’inheritance au travail. Par exemple, si vous utilisiez des modèles et des interfaces identiques pour les fonctions de membre const appropriées, un “Ellipse const &” dans lequel Ellipse serait parfaitement circulaire pourrait être substitué à un “Circle const &”.

Ce que j’essaie de dire ici, c’est qu’il faut beaucoup de reflection et de travail pour maintenir une relation “est-une” substituable entre deux types, que vous utilisiez l’inheritance ou non.

Non Oui. Si la relation est “a-a”, utilisez la composition. ( Ajouté : la version originale de la question disait “utiliser l’inheritance” pour les deux – une simple faute de frappe, mais la correction modifie le premier mot de ma réponse de “Non” à “Oui”.)

Et utilisez la composition par défaut; n’utilisez l’inheritance que lorsque nécessaire (mais n’hésitez pas à l’utiliser).

Les gens disent souvent que l’inheritance est une relation “est-une”, mais cela peut vous causer des ennuis. L’inheritance en C ++ se divise en deux idées: la réutilisation de code et la définition d’interfaces.

Le premier dit “Ma classe est comme cette autre classe. Je vais juste écrire du code pour le delta entre elles et réutiliser autant que possible l’implémentation de l’autre classe de l’autre classe.”

La seconde dépend des classes de base abstraites et est une liste de méthodes que la classe promet d’implémenter.

Le premier peut être très pratique, mais il peut aussi causer des problèmes de maintenance s’il n’est pas bien fait, de sorte que certaines personnes iront jusqu’à dire que vous ne devriez jamais le faire. La plupart des gens insistent sur le second, utilisant l’inheritance pour ce que d’autres langages appellent explicitement une interface.

Je pense que John a la bonne idée. Vous considérez vos relations de classe comme un modèle, ce qu’elles essaient de vous faire faire quand elles commencent à vous enseigner les principes de la PO. Vraiment, vous feriez mieux de voir comment les choses fonctionnent en termes d’ architecture des codes. En d’autres termes, quel est le meilleur moyen pour les autres programmeurs (ou vous-même) de réutiliser votre code sans le casser ou de devoir regarder l’implémentation pour voir ce qu’il fait.

J’ai constaté que l’inheritance casse souvent l’idéal de la “boîte noire”. Si la dissimulation d’informations est importante, ce n’est peut-être pas la meilleure solution. J’ai de plus en plus constaté que l’inheritance était préférable pour fournir une interface commune que pour réutiliser des composants ailleurs.

Bien sûr, chaque situation est unique et il n’existe pas de bonne façon de prendre cette décision, mais seulement des règles empiriques.

Ma règle empirique (bien que non concrète) est que si je peux utiliser le même code pour différentes classes, je le mets dans une classe parente et j’utilise l’inheritance. Sinon j’utilise la composition.

Cela me fait écrire moins de code, ce qui est insortingnsèquement plus facile à gérer.