web-dev-qa-db-fra.com

Odeur de code: abus d'héritage

Il a été généralement accepté dans la communauté OO que l'on devrait "privilégier la composition par rapport à l'héritage". D'autre part, l'héritage fournit à la fois un polymorphisme et une manière directe et concise de tout déléguer à une classe de base sauf si elle est explicitement remplacée et est donc extrêmement pratique et utile. La délégation peut souvent (mais pas toujours) être verbeuse et fragile.

Le signe le plus évident et le plus sûr à mon humble avis d'un abus d'héritage est la violation du principe de substitution de Liskov . Quels sont les autres signes que l'héritage est le mauvais outil pour le travail même si cela semble pratique?

52
dsimcha

Lorsque vous héritez juste pour obtenir des fonctionnalités, vous abusez probablement de l'héritage. Cela conduit à la création d'un God Object .

L'héritage lui-même n'est pas un problème tant que vous voyez une vraie relation entre les classes (comme les exemples classiques, tels que Dog étend Animal) et que vous ne mettez pas sur la classe parent des méthodes qui n'ont pas de sens sur certaines d'entre elles. enfants (par exemple, l'ajout d'une méthode Bark () dans la classe Animal n'aurait aucun sens dans une classe Fish qui étend Animal).

Si une classe a besoin de fonctionnalités, utilisez la composition (peut-être en injectant le fournisseur de fonctionnalités dans le constructeur?). Si une classe doit ÊTRE comme une autre, utilisez l'héritage.

48
Fernando

Je dirais, en prenant le risque d'être abattu, que l'héritage est une odeur de code en soi :)

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)

Avoir un seul mécanisme pour obtenir les deux est ce qui mène en premier lieu à "l'abus d'héritage", car la plupart des gens s'attendent à ce que l'héritage soit lié à l'interface, mais il pourrait être utilisé pour obtenir une implémentation par défaut même par des programmeurs autrement prudents (c'est tellement facile à oubliez ça ...)

En fait, les langues modernes comme Haskell ou Go, ont abandonné l'héritage pour séparer les deux préoccupations.

De retour sur la bonne voie:

Une violation du principe de Liskov est donc le signe le plus sûr, car cela signifie que la partie "interface" du contrat n'est pas respectée.

Cependant, même lorsque l'interface est respectée, vous pouvez avoir des objets héritant des classes de base "fat" simplement parce qu'une des méthodes a été jugée utile.

Par conséquent, le principe de Liskov en lui-même ne suffit pas, ce que vous devez savoir, c'est si oui ou non le polymorphisme est tilisé. Si ce n'est pas le cas, il ne sert à rien d'hériter en premier lieu, c'est un abus.

Sommaire:

  • appliquer le Liskov Principle
  • vérifiez qu'il est effectivement utilisé

Un moyen de le contourner:

Imposer une séparation claire des préoccupations:

  • hériter uniquement des interfaces
  • utiliser la composition pour déléguer la mise en œuvre

signifie qu'au moins quelques frappes sont nécessaires pour chaque méthode, et tout à coup les gens commencent à se demander si c'est une si bonne idée de réutiliser cette classe "grasse" pour juste un petit morceau.

39
Matthieu M.

Est-ce vraiment "généralement accepté"? C'est une idée que j'ai entendue à plusieurs reprises, mais ce n'est guère un principe universellement reconnu. Comme vous venez de le souligner, l'héritage et le polymorphisme sont les caractéristiques de la POO. Sans eux, vous avez juste une programmation procédurale avec une syntaxe maladroite.

La composition est un outil pour construire des objets d'une certaine manière. L'héritage est un outil pour construire des objets d'une manière différente. Il n'y a rien de mal avec l'un ou l'autre; il vous suffit de savoir comment ils fonctionnent et quand il convient d'utiliser chaque style.

14
Mason Wheeler

La force de l'héritage est la réutilisabilité. D'interface, de données, de comportement, de collaborations. C'est un modèle utile à partir duquel transmettre des attributs à de nouvelles classes.

L'inconvénient de l'utilisation de l'héritage est la réutilisabilité. L'interface, les données et le comportement sont encapsulés et transmis en masse, ce qui en fait une proposition tout ou rien. Les problèmes deviennent particulièrement évidents à mesure que les hiérarchies s'approfondissent, que les propriétés qui pourraient être utiles dans l'abstrait le deviennent moins et commencent à obscurcir les propriétés au niveau local.

Chaque classe qui utilise l'objet de composition devra plus ou moins réimplémenter le même code pour interagir avec lui. Bien que cette duplication nécessite une certaine violation de DRY, elle offre aux concepteurs une plus grande liberté pour choisir les propriétés d'une classe les plus pertinentes pour leur domaine. Ceci est particulièrement avantageux lorsque les classes deviennent plus spécialisées.

En général, il devrait être préférable de créer des classes via la composition, puis de convertir en héritage ces classes avec la majeure partie de leurs propriétés en commun, en laissant les différences sous forme de compositions.

IOW, YAGNI (tu ne vas pas avoir besoin d'héritage).

5
Huperniketes

Si vous sous-classez A dans la classe B mais que personne ne se soucie de savoir si B hérite de A ou non, c'est donc le signe que vous vous trompez peut-être .

3
tia

"Privilégier la composition plutôt que l'héritage" est l'affirmation la plus idiote. Ce sont deux éléments essentiels de la POO. C'est comme dire "Privilégiez les marteaux aux scies".

Bien sûr, l'héritage peut être abusé, toute fonctionnalité de langage peut être abusée, les instructions conditionnelles peuvent être abusées.

2
Chuck Stephanski

Le plus évident est peut-être lorsque la classe héritée se retrouve avec une pile de méthodes/propriétés qui ne sont jamais utilisées dans l'interface exposée. Dans le pire des cas, lorsque la classe de base a des membres abstraits, vous trouverez des implémentations vides (ou dénuées de sens) des membres abstraits juste pour `` remplir les blancs '' pour ainsi dire.

Plus subtilement, vous voyez parfois où, dans un effort pour augmenter la réutilisation de code, deux choses qui ont des implémentations similaires ont été regroupées en une seule implémentation - bien que ces deux choses ne soient pas liées. Cela tend à augmenter le couplage de code et à les démêler lorsqu'il s'avère que la similitude n'était qu'une coïncidence peut être assez douloureuse à moins qu'elle ne soit repérée tôt.

Mis à jour avec quelques exemples: bien que cela ne soit pas directement lié à la programmation en tant que telle, je pense que le meilleur exemple pour ce genre de chose est la localisation/internationalisation. En anglais, il y a un mot pour "oui". Dans d'autres langues, il peut y en avoir beaucoup plus pour être d'accord grammaticalement avec la question. Quelqu'un pourrait dire, ah, ayons une chaîne pour "oui" et c'est bien pour beaucoup de langues. Ensuite, ils réalisent que la chaîne "oui" ne correspond pas vraiment à "oui" mais en fait le concept de "la réponse affirmative à cette question" - le fait que c'est "oui" tout le temps est une coïncidence et le temps économisé seulement avoir un "oui" est complètement perdu en démêlant le gâchis qui en résulte.

J'ai vu un exemple particulièrement désagréable de cela dans notre base de code où ce qui était en fait une relation parent -> enfant où le parent et l'enfant avaient des propriétés avec des noms similaires était modélisé avec héritage. Personne ne l'a remarqué car tout semblait fonctionner, alors le comportement de l'enfant (en fait bas) et du parent (sous-classe) devait aller dans des directions différentes. Par chance, cela s'est produit exactement au mauvais moment quand une échéance se préparait - en essayant de modifier le modèle ici et là, la complexité (à la fois en termes de logique et de duplication de code) a immédiatement traversé le toit. Il était également très difficile de raisonner/travailler avec car le code ne se rapportait tout simplement pas à la façon dont l'entreprise pensait ou parlait des concepts représentés par ces classes. En fin de compte, nous avons dû mordre la balle et faire un remaniement majeur qui aurait pu être complètement évité si le gars d'origine n'avait pas décidé que quelques propriétés sonores similaires avec des valeurs qui se trouvaient être les mêmes représentaient le même concept.

0
FinnNk

Je pense que les batailles comme l'héritage contre la composition, ne sont vraiment que des aspects d'une bataille de base plus profonde.

Ce combat est: qu'est-ce qui se traduira par le moins de code, pour la plus grande extensibilité dans les améliorations futures?

C'est pourquoi la question est si difficile à répondre. Dans une langue, il se pourrait que l'héritage soit plus rapidement mis en place que la composition que dans une autre langue, ou vice-versa.

Ou ce pourrait être le problème spécifique que vous résolvez dans le code qui profite davantage de l'opportunité qu'une vision à long terme de la croissance. C'est autant un problème commercial qu'un problème de programmation.

Donc, pour revenir à la question, l'héritage peut être faux s'il vous met dans une veste droite à un moment donné dans le futur - ou s'il crée du code qui n'a pas besoin d'être là (classes héritant des autres sans raison, ou pire encore les classes de base qui commencent à faire beaucoup de tests conditionnels pour les enfants).

Il y a des situations où l'héritage doit être privilégié par rapport à la composition, et la distinction est beaucoup plus nette qu'une question de style. Je paraphrase de Sutter et Alexandrescu C++ Coding Standards ici car ma copie est sur ma bibliothèque au travail en ce moment. Au fait, la lecture est fortement recommandée.

Tout d'abord, l'héritage est déconseillé lorsqu'il n'est pas nécessaire car il crée une relation très intime et étroitement couplée entre les classes de base et dérivées qui peut causer des maux de tête en cours de maintenance. Ce n'est pas seulement l'opinion d'un gars que la syntaxe de l'héritage rend la lecture plus difficile ou quelque chose du genre.

Cela étant dit, l'héritage est encouragé lorsque vous souhaitez que la classe dérivée elle-même soit réutilisée dans des contextes où la classe de base est actuellement utilisée, sans avoir à modifier le code dans ce contexte. Par exemple, si vous avez du code qui commence à ressembler à if (type1) type1.foo(); else if (type2) type2.foo();, vous avez probablement de très bonnes raisons de regarder l'héritage.

En résumé, si vous souhaitez simplement réutiliser des méthodes de classe de base, utilisez la composition. Si vous souhaitez utiliser une classe dérivée dans du code qui utilise actuellement une classe de base, utilisez l'héritage.

0
Karl Bielefeldt