Est-ce que «si une méthode est réutilisée sans modifications, mettez la méthode dans une classe de base, sinon créez une interface» est-ce une bonne règle générale?


10

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 catet dog, qui étendent la classe mammalet ont une seule méthode pet(). Nous ajoutons ensuite la classe alligator, qui n'étend rien et a une seule méthode slither().

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 pour cat, doget alligator, nous devrions créer une classe de base (disons, animal), qui implémente cette méthode.

Cependant, si son implémentation alligatordiffère le moins du monde, nous devons créer une IEatinterface et la créer mammalet l' alligatorimplé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?


27
alligatorLa mise en œuvre de eatdiffère, bien sûr, en ce qu'elle accepte catet en dogtant que paramètres.
sbichenko

1
Lorsque vous souhaitez vraiment qu'une classe de base abstraite partage les implémentations, mais que vous devez utiliser des interfaces pour une extensibilité correcte, vous pouvez souvent utiliser des traits à la place. Autrement dit, si votre langue le prend en charge.
amon

2
Lorsque vous vous peignez dans un coin, il vaut mieux être dans celui le plus proche de la porte.
JeffO

10
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 réutilisation du code devrait alors être réalisée (idéalement) par la composition. Votre collègue a tort.
riwalk

2
Un de mes profs CS a enseigné que les superclasses devraient être is aet que les interfaces sont acts likeou is. Donc un chien is amammifère et acts likeun 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 de isserait The cake is eatableou The book is writable.
MirroredFate

Réponses:


13

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 PetEatingBehaviorrôle qui est de définir les fonctions alimentaires pour les chats et les chiens. Ensuite, vous pouvez avoir IEatet 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 IEatcontiennent moins d'informations que la Mammalclasse 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


Pourriez-vous fournir un exemple de base de PetEatingBehaviormise en œuvre?
sbichenko

1
@exizt Tout d'abord, je suis mal à choisir des noms. C'est PetEatingBehaviorprobablement 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 la eatméthode (instance des dents, instance de l'estomac, etc.). Il ne contient que le code commun aux chats et aux chiens utilisés dans leur eatméthode. Il vous suffit de créer un PetEatingBehaviormembre et de le transférer vers dog.eat/cat.eat.
Simon Bergot

Je ne peux pas croire que ce soit la seule réponse de beaucoup pour éloigner l'OP des héritages :( L'héritage n'est pas pour la réutilisation de code!
Danny Tuppeny

1
@DannyTuppeny Pourquoi pas?
sbichenko

4
@exizt Hériter d'une classe est un gros problème; vous prenez une dépendance (probablement permanente) vis-à-vis de quelque chose dont, dans de nombreuses langues, vous ne pouvez en avoir qu'une. La réutilisation du code est une chose assez banale pour utiliser cette classe de base souvent unique. Et si vous vous retrouvez avec des méthodes où vous souhaitez les partager avec une autre classe, mais qu'elle a déjà une classe de base en raison de la réutilisation d'autres méthodes (ou même, qu'elle fait partie d'une "vraie" hiérarchie utilisée de manière polymorphe (?)). Utilisez l'héritage lorsque ClassA est un type de ClassB (par exemple, une voiture hérite de Vehicle), pas quand elle partage certains détails d'implémentation internes :-)
Danny Tuppeny

11

Vaut-il la peine de suivre cette règle empirique?

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.


1
En relisant cette réponse 3 ans plus tard, je trouve que c'est un bon point: 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 c'est inapproprié de mélanger bon gré mal gré avec d'autres fonctionnalités "
sbichenko

9

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 aet les interfaces sont acts likeou is. Donc un chien is amammifère et acts likeun mangeur. Cela nous dirait que les mammifères devraient être une classe et que le mangeur devrait être une interface. Un exemple de isserait The cake is eatableou The book is writable(faisant les deux eatableet les writableinterfaces).

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.


6

À 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 une foreachboucle.

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:

La composition est souvent plus appropriée que l'héritage. ... [B] car le code implémentant une sous-classe est réparti entre la base et la sous-classe, il peut être plus difficile de comprendre une implémentation. La sous-classe ne peut pas remplacer les fonctions qui ne sont pas virtuelles, donc la sous-classe ne peut pas modifier l'implémentation.

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 dupliquer FileStreamle code de. Les gens intelligents reconnaissent que c'est stupide. Vous devez simplement avoir un FileStreamobjet dans votre classe (en tant que variable locale ou membre) et utiliser ses fonctionnalités.

L' FileStreamexemple 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.


Donc, si je vous comprends bien, l'injection de dépendances offre les mêmes capacités de réutilisation de code que l'héritage. Mais pourquoi utiliseriez-vous l'héritage, alors? Souhaitez-vous fournir un cas d'utilisation? (J'ai vraiment apprécié celui avec FileStream)
sbichenko

1
@exizt, non, il ne fournit pas les mêmes capacités de réutilisation de code. Il offre de meilleures capacités de réutilisation de code (dans de nombreux cas) car il maintient les implémentations bien contenues. Les classes interagissent explicitement avec leurs objets et leur API publique, plutôt que d'acquérir implicitement des capacités et de changer complètement la moitié des fonctionnalités en surchargeant les fonctions existantes.
riwalk

4

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:

public interface IFoodEater
{
    void Eat(IFood food);
}

public class Animal : IFoodEater
{
    private IFoodProcessor _foodProcessor;
    public Animal(IFoodProcessor foodProcessor)
    {
        _foodProcessor = foodProcessor;
    }

    public void Eat(IFood food)
    {
        _foodProcessor.Process(food);
    }
}

Ou implémentez un modèle de visiteur où un FoodProcessoressaie 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.


1
L'IFoodEater est un peu redondant. À quoi d'autre vous attendez-vous pour pouvoir manger? Oui, les animaux comme exemples sont une chose terrible.
Euphoric

1
Et les plantes carnivores? :) Sérieusement, un problème majeur avec la non-utilisation des interfaces dans ce cas est que vous avez intimement lié le concept de manger avec des animaux et qu'il est beaucoup plus difficile d'introduire de nouvelles abstractions pour lesquelles la classe de base animale n'est pas appropriée.
Julia Hayward

1
Je pense que "manger" n'est pas la bonne idée ici: aller plus abstrait. Que diriez-vous d'une IConsumerinterface. Les carnivores peuvent consommer de la viande, les herbivores consomment des plantes et les plantes consomment du soleil et de l'eau.

2

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:

public abstract class Person
    {
        /// <summary>
        /// Inheritors must implement a hello
        /// </summary>
        /// <returns>Hello</returns>
        public abstract string SayHello();
        /// <summary>
        /// Inheritors can use base functionality, or override
        /// </summary>
        /// <returns></returns>
        public virtual string SayGoodBye()
        {
            return "Good Bye";
        }
        /// <summary>
        /// Base Functionality that is inherited 
        /// </summary>
        public void DoSomething()
        {
            //Do Something Here
        }
    }

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.


1

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.


Attendez, pourquoi ré-implémenter eat()dans chaque animal? Nous pouvons le faire mammalimplémenter, non? Je comprends cependant votre point sur les exigences.
sbichenko

@exizt: Désolé, j'ai mal lu votre question.
Euphoric

1

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.


1

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:

  1. 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.

  2. 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 .

  3. 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.


5
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.
Simon Bergot

1
"Cela ne viole pas le principe de substitution de Liskov." Mais il est extrêmement proche de devenir une violation de LSP. Mieux vaut prévenir que guérir.
Euphoric

1

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.

IAnimalinterface 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' IAnimalinterface 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.


0

Je ne suis pas sûr que ce soit la meilleure règle de base. Vous pouvez mettre une abstractméthode dans la classe de base et demander à chaque classe de l'implémenter. Puisque tout le monde mammaldoit manger, il serait logique de le faire. Ensuite, chacun mammalpeut utiliser sa seule eatmé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.


-1

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.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.