pourquoi dériver d’une classe concrète est une mauvaise conception

Je lisais à propos du modèle d’interface non virtuelle : Herb Sutter explique pourquoi la fonction virtuelle doit être privée dans la plupart des cas, protégée dans certains cas et jamais publique.

Mais à la fin de l’article, il écrit:

Ne dérivez pas de cours concrets. Ou, comme le dit Scott Meyers à la rubrique 33 de More Effective C ++, [8] “Rendre les classes non-feuilles abstraites”. (Certes, cela peut arriver dans la pratique – dans du code écrit par quelqu’un d’autre, bien sûr, pas par vous! – et dans ce cas, vous devrez peut-être avoir un destructeur virtuel public juste pour prendre en charge ce qui est déjà une mauvaise conception. Mieux vaut refactoriser et réparer le design, si vous le pouvez.)

Mais je ne comprends pas pourquoi c’est une mauvaise conception

Un problème avec l’inheritance d’un type concret est qu’il crée une ambiguïté quant à savoir si un code spécifiant un type donné veut vraiment un object du type concret spécifique ou veut un object d’un type qui se comporte de la même manière que le type concret . Cette distinction est essentielle en C ++, car il existe de nombreux cas où des opérations qui fonctionneront correctement sur des objects d’un certain type échoueront mal sur des objects de types dérivés. En Java et .NET, il y a beaucoup moins de situations dans lesquelles on peut utiliser un object d’un type particulier sans pouvoir utiliser un object d’un type dérivé. En tant que tel, hériter d’un type concret n’est pas aussi problématique. Même là, cependant, avoir des classes concrètes scellées qui héritent des classes abstraites et utiliser les types de classe abstraits partout sauf dans les invocations de constructeurs (qui doivent utiliser les types concrets) facilitera la modification de parties de la hiérarchie de classes sans casser le code.

Vous pouvez acheter une copie de More Effective C ++ ou consulter votre bibliothèque locale pour en obtenir une copie et lire le point 33 pour une explication complète. L’explication donnée ici est que cela rend votre classe sujette à une affectation partielle, également appelée découpage en tranches:

Ici, nous avons deux problèmes. Premièrement, l’opérateur d’affectation invoqué sur la dernière ligne est celui de la classe Animal , même si les objects impliqués sont de type Lizard . En conséquence, seule la partie Animal de liz1 sera modifiée. Ceci est une cession partielle. Après la cession, les membres Animal liz1 les liz1 valeurs que liz2 , mais les membres Lizard liz1 restnt inchangés.

SecondLe deuxième problème est que les vrais programmeurs écrivent un code comme celui-ci. Il n’est pas rare d’affecter des objects à des objects via des pointeurs, en particulier pour les programmeurs C expérimentés qui sont passés à C ++. Cela étant, nous aimerions que la mission se comporte de manière plus raisonnable. Comme indiqué au point 32, nos classes doivent être faciles à utiliser correctement et difficiles à utiliser incorrectement, et les classes de la hiérarchie ci-dessus sont faciles à utiliser incorrectement.

Voir cette question et d’autres pour une description du problème de découpage d’object en C ++.

Le point 33 dit aussi ceci, plus tard:

Le remplacement d’une classe de base concrète telle que Animal par une classe de base abstraite telle que AbstractAnimal offre des avantages bien au-delà de la simple simplification du comportement de l’ operator= . Cela réduit également les chances que vous essayiez de traiter les tableaux de manière polymorphe, dont les conséquences désagréables sont examinées à la rubrique 3. Le principal avantage de la technique réside toutefois au niveau de la conception, car le remplacement des classes de base de béton par des bases abstraites classes vous oblige à reconnaître explicitement l’existence d’abstractions utiles. En d’autres termes, cela vous oblige à créer de nouvelles classes abstraites pour des concepts utiles, même si vous n’êtes pas conscient du fait que les concepts utiles existent.

Je pense que c’est parce que la classe concrète a un comportement concret. Lorsque vous en dérivez, vous vous engagez à conserver le même “contrat”, mais le contrat lui-même est défini par l’implémentation spécifique de la classe de base, et comporte en réalité de nombreuses subtilités que vous romprez probablement sans le savoir.

Disclaimer: Je ne suis pas un développeur expérimenté; c’est juste une suggestion.

En général, il est difficile de maintenir ssortingctement un contrat entre deux objects concrets distincts.

L’inheritance devient vraiment plus facile et plus robuste lorsque nous traitons un comportement générique.

Imaginez que nous voulions créer une classe nommée Ferrari et une sous-classe nommée SuperFerrari . Le contrat est: turnTheKey() , goFast() , skid()

À première vue, cela ressemble à une classe très similaire, sans conflit. Allons avec inheritance de ces deux classes concrètes.

Cependant, nous voulons maintenant append une fonctionnalité à SuperFerrari : turnOnBluRayDisc() .

Problème: l’inheritance attend une relation IS-A entre les composants. Avec cette nouvelle fonctionnalité, SuperFerrari n’est plus une simple Ferrari avec son propre comportement; il ajoute maintenant le comportement. Cela conduirait à un code laid, avec une dissortingbution nécessaire, afin de choisir la nouvelle fonctionnalité tout en traitant initialement une référence Ferrari (polymorphism).

Avec une classe abstraite / interface commune aux deux classes, ce problème disparaîtrait puisque Ferrari et SuperFerrari auraient deux feuilles, ce qui est plus logique puisque Ferrari et SuperFerrari ne doivent plus être considérés comme similaires (et non plus comme une relation IS-A). .

En bref, dériver de la classe concrète mènerait dans de nombreux cas à un code médiocre / laid, moins flexible, et difficile à lire et à maintenir.

Créer une hiérarchie basée sur une classe / interface abstraite permet aux classes d’enfants concrètes d’évoluer séparément sans problème, en particulier lorsque vous implémentez des sous-hiérarchies dédiées à un nombre particulier de classes concrètes sans ennuyer les autres feuilles, tout en bénéficiant du polymorphism.

Lorsque vous dérivez d’une classe de base concrète, vous héritez de fonctionnalités qui ne vous seront peut-être pas révélées si vous ne possédez pas le code source. Lorsque vous programmez sur une interface (ou une classe abstraite), vous n’héritez pas de la fonctionnalité, ce qui en fait un meilleur moyen de faire des choses comme exposer une API. Cela étant dit, je pense qu’il y a beaucoup de fois où hériter d’une classe concrète est acceptable