Un de mes collègues est venu avec une règle de base pour choisir entre créer une classe de base ou une interface.
Il dit:
Imaginez chaque nouvelle méthode que vous vous apprêtez à mettre en œuvre. Pour chacun d'eux, considérez ceci: cette méthode sera-t-elle implémentée par plus d'une classe exactement sous cette forme, sans aucun changement? Si la réponse est "oui", créez une classe de base. Dans toutes les autres situations, créez une interface.
Par exemple:
Considérez les classes
cat
etdog
, qui étendent la classemammal
et ont une seule méthodepet()
. Nous ajoutons ensuite la classealligator
, qui n'étend rien et a une seule méthodeslither()
.Maintenant, nous voulons ajouter une
eat()
méthode à chacun d'eux.Si l'implémentation de la
eat()
méthode sera exactement la même pourcat
,dog
etalligator
, nous devrions créer une classe de base (disons,animal
), qui implémente cette méthode.Cependant, si son implémentation
alligator
diffère le moins du monde, nous devons créer uneIEat
interface et la créermammal
et l'alligator
implémenter.
Il insiste sur le fait que cette méthode couvre tous les cas, mais cela me semble une simplification excessive.
Vaut-il la peine de suivre cette règle empirique?
la source
alligator
La mise en œuvre deeat
diffère, bien sûr, en ce qu'elle acceptecat
et endog
tant que paramètres.is a
et que les interfaces sontacts like
ouis
. Donc un chienis a
mammifère etacts like
un mangeur. Cela nous dirait que les mammifères devraient être une classe et que le mangeur devrait être une interface. Cela a toujours été un guide très utile. Sidenote: Un exemple deis
seraitThe cake is eatable
ouThe book is writable
.Réponses:
Je ne pense pas que ce soit une bonne règle d'or. Si vous êtes préoccupé par la réutilisation du code, vous pouvez implémenter un
PetEatingBehavior
rôle qui est de définir les fonctions alimentaires pour les chats et les chiens. Ensuite, vous pouvez avoirIEat
et réutiliser le code ensemble.Ces jours-ci, je vois de moins en moins de raisons d'utiliser l'héritage. Un gros avantage est la facilité d'utilisation. Prenez un cadre GUI. Une façon populaire de concevoir une API pour cela est d'exposer une énorme classe de base et de documenter les méthodes que l'utilisateur peut remplacer. Nous pouvons donc ignorer toutes les autres choses complexes que notre application doit gérer, car une implémentation "par défaut" est donnée par la classe de base. Mais nous pouvons toujours personnaliser les choses en réimplémentant la bonne méthode lorsque nous en avons besoin.
Si vous vous en tenez à l'interface de votre API, votre utilisateur a généralement plus de travail à faire pour faire fonctionner des exemples simples. Mais les API avec interfaces sont souvent moins couplées et plus faciles à maintenir pour la simple raison qu'elles
IEat
contiennent moins d'informations que laMammal
classe de base. Ainsi, les consommateurs dépendront d'un contrat plus faible, vous aurez plus d'opportunités de refactoring, etc.Pour citer Rich Hickey: Simple! = Easy
la source
PetEatingBehavior
mise en œuvre?PetEatingBehavior
probablement le mauvais. Je ne peux pas vous donner d'implémentation car ce n'est qu'un exemple de jouet. Mais je peux décrire les étapes de refactoring: il a un constructeur qui prend toutes les informations utilisées par les chats / chiens pour définir laeat
méthode (instance des dents, instance de l'estomac, etc.). Il ne contient que le code commun aux chats et aux chiens utilisés dans leureat
méthode. Il vous suffit de créer unPetEatingBehavior
membre et de le transférer vers dog.eat/cat.eat.C'est une règle d'or décente, mais je connais pas mal d'endroits où je la viole.
Pour être honnête, j'utilise beaucoup plus de classes de base (abstraites) que mes pairs. Je fais cela comme une programmation défensive.
Pour moi (dans de nombreux langages), une classe de base abstraite ajoute la contrainte clé qu'il ne peut y avoir qu'une seule classe de base. En choisissant d'utiliser une classe de base plutôt qu'une interface, vous dites "c'est la fonctionnalité de base de la classe (et de tout héritier), et il est inapproprié de mélanger bon gré mal gré avec d'autres fonctionnalités". Les interfaces sont à l'opposé: "C'est un trait d'un objet, pas nécessairement sa responsabilité principale".
Ce type de choix de conception crée des guides implicites pour vos implémenteurs sur la façon dont ils doivent utiliser votre code et, par extension, écrivent leur code. En raison de leur plus grand impact sur la base de code, j'ai tendance à prendre ces éléments en considération plus fortement lors de la prise de décision de conception.
la source
Le problème avec la simplification de votre ami est qu'il est extrêmement difficile de déterminer si une méthode devra ou non changer. Il vaut mieux utiliser une règle qui parle de la mentalité derrière les superclasses et les interfaces.
Au lieu de déterminer ce que vous devez faire en regardant ce que vous pensez être la fonctionnalité, regardez la hiérarchie que vous essayez de créer.
Voici comment un de mes professeurs a enseigné la différence:
Les superclasses doivent être
is a
et les interfaces sontacts like
ouis
. Donc un chienis a
mammifère etacts like
un mangeur. Cela nous dirait que les mammifères devraient être une classe et que le mangeur devrait être une interface. Un exemple deis
seraitThe cake is eatable
ouThe book is writable
(faisant les deuxeatable
et leswritable
interfaces).L'utilisation d'une méthodologie comme celle-ci est raisonnablement simple et facile, et vous oblige à coder sur la structure et les concepts plutôt que sur ce que vous pensez que le code fera. Cela rend la maintenance plus facile, le code plus lisible et la conception plus simple à créer.
Indépendamment de ce que le code pourrait réellement dire, si vous utilisez une méthode comme celle-ci, vous pourriez revenir dans cinq ans et utiliser la même méthode pour retracer vos étapes et comprendre facilement comment votre programme a été conçu et comment il interagit.
Juste mes deux cents.
la source
À la demande d'exizt, j'élargis mon commentaire à une réponse plus longue.
La plus grande erreur que les gens commettent est de croire que les interfaces sont simplement des classes abstraites vides. Une interface est un moyen pour un programmeur de dire: "Je me fiche de ce que vous me donnez, tant qu'il suit cette convention."
La bibliothèque .NET est un merveilleux exemple. Par exemple, lorsque vous écrivez une fonction qui accepte un
IEnumerable<T>
, ce que vous dites est: «Je me fiche de la façon dont vous stockez vos données. Je veux juste savoir que je peux utiliser uneforeach
boucle.Cela conduit à un code très flexible. Du coup pour être intégré, il suffit de respecter les règles des interfaces existantes. Si la mise en œuvre de l'interface est difficile ou déroutante, c'est peut-être un indice que vous essayez de pousser une cheville carrée dans un trou rond.
Mais la question se pose alors: "Qu'en est-il de la réutilisation du code? Mes professeurs CS m'ont dit que l'héritage était la solution à tous les problèmes de réutilisation du code et que l'héritage vous permet d'écrire une fois et de l'utiliser partout et qu'il guérirait la ménangite en train de sauver des orphelins des mers montantes et il n'y aurait plus de larmes et ainsi de suite etc. etc. etc. "
Utiliser l'héritage simplement parce que vous aimez le son des mots "réutilisation de code" est une très mauvaise idée. Le guide de style de code de Google explique ce point de manière assez concise:
Pour illustrer pourquoi l'héritage n'est pas toujours la réponse, je vais utiliser une classe appelée MySpecialFileWriter †. Une personne qui croit aveuglément que l'héritage est la solution à tous les problèmes dirait que vous devriez essayer d'hériter
FileStream
, de peur de dupliquerFileStream
le code de. Les gens intelligents reconnaissent que c'est stupide. Vous devez simplement avoir unFileStream
objet dans votre classe (en tant que variable locale ou membre) et utiliser ses fonctionnalités.L'
FileStream
exemple peut sembler artificiel, mais ce n'est pas le cas. Si vous avez deux classes qui implémentent la même interface de la même manière, vous devez avoir une troisième classe qui encapsule toute opération dupliquée. Votre objectif devrait être d'écrire des classes qui sont des blocs réutilisables autonomes qui peuvent être assemblés comme des legos.Cela ne signifie pas que l'héritage doit être évité à tout prix. Il y a de nombreux points à considérer, et la plupart seront couverts par la recherche de la question, "Composition vs héritage". Notre propre Stack Overflow a quelques bonnes réponses à ce sujet.
À la fin de la journée, les sentiments de votre collègue manquent de profondeur ou de compréhension nécessaires pour prendre une décision éclairée. Recherchez le sujet et découvrez-le par vous-même.
† Pour illustrer l'héritage, tout le monde utilise des animaux. C'est inutile. En 11 ans de développement, je n'ai jamais écrit de classe nommée
Cat
, donc je ne vais pas l'utiliser comme exemple.la source
FileStream
)Les exemples soulignent rarement, surtout pas lorsqu'ils concernent des voitures ou des animaux. La façon dont un animal mange (ou transforme les aliments) n'est pas vraiment liée à son héritage, il n'y a pas de façon unique de manger qui s'appliquerait à tous les animaux. Cela dépend davantage de ses capacités physiques (les modes d'entrée, de traitement et de sortie pour ainsi dire). Les mammifères peuvent être des carnivores, des herbivores ou des omnivores par exemple, tandis que les oiseaux, les mammifères et les poissons peuvent manger des insectes. Ce comportement peut être capturé avec certains modèles.
Dans ce cas, vous êtes mieux en faisant un comportement abstrait, comme ceci:
Ou implémentez un modèle de visiteur où un
FoodProcessor
essaie de nourrir des animaux compatibles (ICarnivore : IFoodEater
,MeatProcessingVisitor
). La pensée d'animaux vivants peut vous empêcher de penser à déchirer leur système digestif et à le remplacer par un générique, mais vous leur rendez vraiment service.Cela fera de votre animal une classe de base, et encore mieux, vous n'aurez plus jamais à changer lors de l'ajout d'un nouveau type de nourriture et d'un processeur correspondant, afin que vous puissiez vous concentrer sur les choses qui font de cette classe animale spécifique que vous êtes travailler si unique.
Alors oui, les classes de base ont leur place, la règle de base s'applique (souvent, pas toujours, car c'est une règle de base) mais continuez à chercher des comportements que vous pouvez isoler.
la source
IConsumer
interface. Les carnivores peuvent consommer de la viande, les herbivores consomment des plantes et les plantes consomment du soleil et de l'eau.Une interface est vraiment un contrat. Il n'y a aucune fonctionnalité. C'est à l'implémenteur de mettre en œuvre. Il n'y a pas de choix car il faut exécuter le contrat.
Les classes abstraites donnent aux implémenteurs des choix. On peut choisir d'avoir une méthode que tout le monde doit implémenter, fournir une méthode avec des fonctionnalités de base que l'on pourrait remplacer, ou fournir des fonctionnalités de base que tous les héritiers peuvent utiliser.
Par exemple:
Je dirais que votre règle de base pourrait également être gérée par une classe de base. En particulier, car de nombreux mammifères "mangent" probablement la même chose qu'utiliser une classe de base peut être mieux qu'une interface, car on pourrait implémenter une méthode de manger virtuelle que la plupart des héritiers pourraient utiliser et les cas exceptionnels pourraient alors passer outre.
Maintenant, si la définition de «manger» varie considérablement d'un animal à l'autre, alors peut-être que l'interface serait meilleure. Il s'agit parfois d'une zone grise et dépend de la situation individuelle.
la source
Non.
Les interfaces concernent la façon dont les méthodes sont implémentées. Si vous basez votre décision sur le moment où utiliser l'interface sur la façon dont les méthodes seront implémentées, alors vous faites quelque chose de mal.
Pour moi, la différence entre l'interface et la classe de base concerne la position des exigences. Vous créez une interface, lorsqu'un élément de code nécessite une classe avec une API spécifique et un comportement implicite qui émerge de cette API. Vous ne pouvez pas créer une interface sans code qui l'appellera réellement.
D'un autre côté, les classes de base sont généralement conçues comme un moyen de réutiliser du code, donc vous vous embêtez rarement avec du code qui appellera cette classe de base et ne prendra en compte que la haute recherche d'objets dont cette classe de base fait partie.
la source
eat()
dans chaque animal? Nous pouvons le fairemammal
implémenter, non? Je comprends cependant votre point sur les exigences.Ce n'est pas vraiment une bonne règle de base, car si vous voulez différentes implémentations, vous pouvez toujours avoir une classe de base abstraite avec une méthode abstraite. Chaque héritier est garanti d'avoir cette méthode, mais chacun définit sa propre implémentation. Techniquement, vous pourriez avoir une classe abstraite avec rien d'autre qu'un ensemble de méthodes abstraites, et ce serait essentiellement la même chose qu'une interface.
Les classes de base sont utiles lorsque vous souhaitez une implémentation réelle d'un état ou d'un comportement pouvant être utilisé dans plusieurs classes. Si vous vous retrouvez avec plusieurs classes qui ont des champs et des méthodes identiques avec des implémentations identiques, vous pouvez probablement refactoriser cela en une classe de base. Vous devez juste faire attention à la façon dont vous héritez. Chaque champ ou méthode existant dans la classe de base doit avoir un sens dans l'héritier, en particulier lorsqu'il est converti en classe de base.
Les interfaces sont utiles lorsque vous devez interagir avec un ensemble de classes de la même manière, mais vous ne pouvez pas prévoir ou ne pas vous soucier de leur implémentation réelle. Prenons par exemple quelque chose comme une interface Collection. Lord ne connaît que les innombrables façons dont quelqu'un pourrait décider de mettre en œuvre une collection d'objets. Liste liée? Liste des tableaux? Empiler? Queue? Cette méthode SearchAndReplace ne se soucie pas, il suffit de lui passer quelque chose qu'elle peut appeler Add, Remove et GetEnumerator.
la source
Je regarde en quelque sorte les réponses à cette question moi-même, pour voir ce que les autres ont à dire, mais je vais dire trois choses que je suis enclin à y penser:
Les gens mettent beaucoup trop de foi dans les interfaces et trop peu de foi dans les classes. Les interfaces sont essentiellement des classes de base très édulcorées, et il y a des occasions où l'utilisation de quelque chose de léger peut conduire à des choses comme un code passe-partout excessif.
Il n'y a rien de mal à remplacer une méthode, à l'appeler
super.method()
et à laisser le reste de son corps faire des choses qui n'interfèrent pas avec la classe de base. Cela ne viole pas le principe de substitution de Liskov .En règle générale, être aussi rigide et dogmatique en génie logiciel est une mauvaise idée, même si quelque chose est généralement une meilleure pratique.
Je prendrais donc ce qui a été dit avec un grain de sel.
la source
People put way too much faith into interfaces, and too little faith into classes.
Drôle. J'ai tendance à penser que le contraire est vrai.Je veux adopter une approche linguistique et sémantique à cet égard. Une interface n'est pas là pour
DRY
. Bien qu'il puisse être utilisé comme mécanisme de centralisation parallèlement à une classe abstraite qui l'implémente, il n'est pas destiné à DRY.Une interface est un contrat.
IAnimal
interface est un contrat tout ou rien entre alligator, chat, chien et la notion d'animal. Ce qu'il dit est essentiellement "si vous voulez être un animal, soit vous devez avoir tous ces attributs et caractéristiques, soit vous n'êtes plus un animal".L'héritage de l'autre côté est un mécanisme de réutilisation. Dans ce cas, je pense qu'avoir un bon raisonnement sémantique peut vous aider à décider quelle méthode doit aller où. Par exemple, alors que tous les animaux peuvent dormir et dormir de la même façon, je n'utiliserai pas de classe de base pour cela. J'ai d'abord mis la
Sleep()
méthode dans l'IAnimal
interface comme un accent (sémantique) sur ce qu'il faut pour être un animal, puis j'utilise une classe abstraite de base pour chat, chien et alligator comme technique de programmation pour ne pas me répéter.la source
Je ne suis pas sûr que ce soit la meilleure règle de base. Vous pouvez mettre une
abstract
méthode dans la classe de base et demander à chaque classe de l'implémenter. Puisque tout le mondemammal
doit manger, il serait logique de le faire. Ensuite, chacunmammal
peut utiliser sa seuleeat
méthode. J'utilise généralement uniquement des interfaces pour la communication entre les objets, ou comme marqueur pour marquer une classe comme quelque chose. Je pense que la surutilisation des interfaces rend le code bâclé.Je suis également d'accord avec @Panzercrisis qu'une règle de base ne devrait être qu'une ligne directrice générale. Pas quelque chose qui devrait être écrit dans la pierre. Il y aura sans aucun doute des moments où cela ne fonctionnera pas.
la source
Les interfaces sont des types. Ils ne fournissent aucune implémentation. Ils définissent simplement le contrat de gestion de ce type. Ce contrat doit être respecté par toute classe implémentant l'interface.
L'utilisation d'interfaces et de classes abstraites / de base doit être déterminée par les exigences de conception.
Une seule classe ne nécessite pas d'interface.
Si deux classes sont interchangeables au sein d'une fonction, elles doivent implémenter une interface commune. Toute fonctionnalité implémentée de manière identique pourrait alors être refactorisée en une classe abstraite qui implémente l'interface. L'interface pourrait être considérée comme redondante à ce stade s'il n'y a pas de fonctionnalité distinctive. Mais alors quelle est la différence entre les deux classes?
L'utilisation de classes abstraites comme types est généralement compliquée à mesure que de nouvelles fonctionnalités sont ajoutées et qu'une hiérarchie se développe.
Privilégiez la composition plutôt que l'héritage - tout cela disparaît.
la source