Est-il judicieux de définir une interface si j'ai déjà une classe abstraite?

12

J'ai une classe avec des fonctionnalités par défaut / partagées. J'utilise abstract classpour cela:

public interface ITypeNameMapper
{
    string Map(TypeDefinition typeDefinition);
}

public abstract class TypeNameMapper : ITypeNameMapper
{
    public virtual string Map(TypeDefinition typeDefinition)
    {
        if (typeDefinition is ClassDefinition classDefinition)
        {
            return Map(classDefinition);
        }
        ...

        throw new ArgumentOutOfRangeException(nameof(typeDefinition));
    }

    protected abstract string Map(ClassDefinition classDefinition);
}

Comme vous pouvez le voir, j'ai également l'interface ITypeNameMapper. Est-il judicieux de définir cette interface si j'ai déjà une classe abstraite TypeNameMapperou si abstract classc'est juste assez?

TypeDefinition dans cet exemple minimal est également abstrait.

Konrad
la source
4
Juste pour info - dans OOD, la vérification des types dans le code est une indication que votre hiérarchie de classe n'est pas correcte - cela vous dit de créer des classes dérivées à partir du type que vous vérifiez. Donc, dans ce cas, vous voulez une interface ITypeMapper qui spécifie une méthode Map (), puis implémentez cette interface dans les classes TypeDefinition et ClassDefinition. De cette façon, votre ClassAssembler parcourt simplement la liste des ITypeMappers, en appelant Map () sur chacun, et obtient la chaîne souhaitée des deux types car ils ont chacun leur propre implémentation.
Rodney P. Barbati
1
Utiliser une classe de base à la place d'une interface, sauf si vous en avez absolument besoin, s'appelle "graver la base". Si cela peut être fait avec une interface, faites-le avec une interface. La classe abstraite devient alors un supplément utile. artima.com/intv/dotnet.html Plus précisément, l' accès à distance nécessite que vous dériviez de MarshalByRefObject, donc si vous voulez être éloigné, vous ne pouvez pas dériver d'autre chose.
Ben
@ RodneyP.Barbati ne fonctionnera pas si je veux avoir de nombreux mappeurs pour le même type que je peux changer en fonction de la situation
Konrad
1
@Ben brûle la base qui est intéressante. Merci pour le bijou!
Konrad
@Konrad C'est incorrect - rien ne vous empêche d'implémenter l'interface en appelant une autre implémentation de l'interface, par conséquent, chaque type peut avoir une implémentation enfichable que vous exposez via l'implémentation dans le type.
Rodney P. Barbati

Réponses:

31

Oui, car C # n'autorise pas l'héritage multiple sauf avec les interfaces.

Donc, si j'ai une classe qui est à la fois TypeNameMapper et SomethingelseMapper, je peux faire:

class MultiFunctionalClass : ITypeNameMapper, ISomethingelseMapper 
{
    private TypeNameMapper map1
    private SomethingelseMapper map2

    public string Map(TypeDefinition typeDefinition) { return map1.Map(typeDefintion);}

    public string Map(OtherDef otherDef) { return map2.Map(orderDef); }
}
Ewan
la source
4
@Liath: La responsabilité unique est en fait un facteur qui explique pourquoi l'héritage est nécessaire. Prenons trois traits possibles: ICanFly, ICanRun, ICanSwim. Vous devez créer 8 classes de créatures, une pour chaque combinaison de traits (YYY, YYN, YNY, ..., NNY, NNN). SRP indique que vous implémentez chaque comportement (voler, courir, nager) une fois puis les implémenter de manière réutilisable sur les 8 créatures. La composition serait encore meilleure ici, mais en ignorant la composition pendant une seconde, vous devriez déjà voir la nécessité d'hériter / implémenter plusieurs comportements réutilisables séparés en raison de SRP.
Flater
4
@Konrad, c'est ce que je veux dire. Si vous écrivez l'interface et n'en avez jamais besoin, le coût est de 3 LoC. Si vous n'écrivez pas l'interface et que vous en avez besoin, le coût pourrait être de refactoriser l'ensemble de votre application.
Ewan
3
(1) La mise en œuvre des interfaces est un héritage? (2) La question et la réponse doivent toutes deux être nuancées. "Ça dépend." (3) L'héritage multiple nécessite un autocollant d'avertissement. (4) Je pourrais vous montrer des K de interfacefaute professionnelle craptaculaire .. des implémentations redondantes qui devraient être dans la base - qui évoluent indépendamment !, des changements de logique conditionnelle conduisant cette ordure faute de méthodes de modèle, d'encapsulation cassée, d'abstraction inexistante. Mon préféré: les classes à 1 méthode implémentant chacune sa propre méthode à 1 interface, chacune avec une seule instance. ... etc, etc, et etc.
radarbob
3
Il y a un coefficient de friction invisible dans le code. Vous le ressentez lorsqu'il est contraint de lire des interfaces superflues. Ensuite, il se montre en train de chauffer comme la navette spatiale claquant dans l'atmosphère alors qu'une implémentation d'interface luit la classe abstraite. C'est la Twilight Zone et voici la navette Challenger. En acceptant mon sort, je regarde le plasma qui fait rage avec une émerveillement intrépide et détaché. Des frottements impitoyables déchirent la façade de la perfection, déposant la carcasse brisée en production avec un coup de tonnerre.
radarbob
4
@radarbob Poetic. ^ _ ^ Je serais d'accord; bien que le coût des interfaces soit faible, il n'est pas inexistant. Pas tellement en les écrivant, mais dans une situation de débogage, c'est 1-2 étapes supplémentaires pour arriver au comportement réel, qui peuvent s'additionner rapidement lorsque vous obtenez une demi-douzaine de niveaux d'abstraction en profondeur. Dans une bibliothèque, ça vaut généralement le coup. Mais dans une application, le refactoring est préférable à une généralisation prématurée.
Errorsatz
1

Les interfaces et les classes abstraites ont différentes fonctions:

  • Les interfaces définissent les API et appartiennent aux clients et non aux implémentations.
  • Si les classes partagent des implémentations, vous pouvez bénéficier d'une classe abstraite .

Dans votre exemple, interface ITypeNameMapperdéfinit les besoins des clients et abstract class TypeNameMappern'ajoute aucune valeur.

Geoffrey Hale
la source
2
Les classes abstraites appartiennent également aux clients. Je ne sais pas ce que tu veux dire par là. Tout ce qui publicappartient au client. TypeNameMapperajoute beaucoup de valeur car il évite à l'implémenteur d'écrire une logique commune.
Konrad
2
Qu'une interface soit présentée comme interfacenon ou non n'est pertinent que dans la mesure où C # interdit l'héritage multiple complet.
Déduplicateur
0

L'ensemble du concept d'interfaces a été créé afin de supporter une famille de classes ayant une API partagée.

Cela signifie explicitement que toute utilisation d'une interface implique qu'il y a (ou devrait avoir) plus d'une implémentation de sa spécification.

Les cadres DI ont brouillé les eaux ici dans la mesure où beaucoup d'entre eux nécessitent une interface même s'il n'y aura qu'une seule implémentation - pour moi, cela représente un surcoût déraisonnable pour ce qui, dans la plupart des cas, n'est qu'un moyen plus complexe et plus lent d'appeler le nouveau, mais il c'est ce que c'est.

La réponse réside dans la question elle-même. Si vous avez une classe abstraite, vous vous préparez à la création de plusieurs classes dérivées avec une API commune. Ainsi, l'utilisation d'une interface est clairement indiquée.

Rodney P. Barbati
la source
2
"Si vous avez une classe abstraite, ... l'utilisation d'une interface est clairement indiquée." - Ma conclusion est le contraire: si vous avez une classe abstraite, alors utilisez-la comme si c'était une interface (si la DI ne joue pas avec elle, envisagez une implémentation différente). Ce n'est que lorsque cela ne fonctionne vraiment pas, créez l'interface - vous pouvez le faire à tout moment plus tard.
maaartinus
1
Idem, @maaartinus. Et même pas "... comme s'il s'agissait d'une interface". Les membres publics d'une classe sont une interface. Idem également pour "vous pouvez le faire à tout moment plus tard"; cela va au cœur des principes fondamentaux du design 101.
radarbob
@maaartinus: Si l'on crée une interface depuis le début, mais utilise partout des références de type classe abstraite, l'existence de l'interface n'aidera en rien. Si, cependant, le code client utilise le type d'interface au lieu du type de classe abstraite dans la mesure du possible, cela facilitera grandement les choses s'il devient nécessaire de produire une implémentation qui est très différente en interne de la classe abstraite. Changer le code client à ce stade pour utiliser l'interface sera beaucoup plus pénible que de concevoir du code client pour le faire dès le départ.
supercat
1
@supercat Je pourrais être d'accord en supposant que vous écrivez une bibliothèque. S'il s'agit d'une application, je ne vois aucun problème pour remplacer toutes les occurrences à la fois. Mon Eclipse peut le faire (Java, pas C #), mais si ce n'est pas le cas, c'est comme find ... -exec perl -pi -e ...et éventuellement corriger des erreurs de compilation triviales. Mon point est: L'avantage de ne pas avoir une chose (actuellement) inutile est beaucoup plus grand que les économies futures possibles.
maaartinus
La première phrase serait correcte, si vous marquiez en interfacetant que code (vous parlez de C # - interfaces, pas une interface abstraite, comme tout ensemble de classes / fonctions / etc), et la terminiez "... mais toujours hors la loi du multiple complet héritage." Il n'y a pas besoin de interfaces si vous avez un IM.
Déduplicateur
0

Si nous voulons répondre explicitement à la question, l'auteur dit "Est-il judicieux de définir cette interface si j'ai déjà une classe abstraite TypeNameMapper ou une classe abstraite est juste suffisante?"

La réponse est oui et non - Oui, vous devez créer l'interface même si vous avez déjà la classe de base abstraite (car vous ne devez pas faire référence à la classe de base abstraite dans aucun code client), et non, car vous ne devriez pas avoir créé la classe de base abstraite en l'absence d'une interface.

Le fait que vous n'ayez pas suffisamment réfléchi à l'API que vous essayez de créer est évident. Proposez d'abord l'API, puis fournissez une implémentation partielle si vous le souhaitez. Le fait que votre classe de base abstraite vérifie le code dans le code suffit pour vous dire que ce n'est pas la bonne abstraction.

Comme dans la plupart des choses dans OOD, lorsque vous êtes dans le groove et que votre modèle d'objet est bien fait, votre code vous informera de ce qui va suivre. Commencez avec une interface, implémentez cette interface dans les classes dont vous avez besoin. Si vous vous retrouvez en train d'écrire du code similaire, extrayez-le dans une classe de base abstraite - c'est l'interface qui est importante, la classe de base abstraite n'est qu'une aide et il peut y en avoir plus d'une.

Rodney P. Barbati
la source
-1

Il est assez difficile de répondre sans connaître le reste de l'application.

En supposant que vous utilisez une sorte de modèle DI et que vous avez conçu votre code pour qu'il soit aussi extensible que possible (afin que vous puissiez en ajouter TypeNameMapperfacilement), je dirais oui.

Raisons pour:

  • Vous l'obtenez effectivement gratuitement, car la classe de base implémente le dont interfacevous n'avez pas besoin de vous en soucier dans les implémentations enfants
  • La plupart des frameworks IoC et des bibliothèques de simulation s'attendent à ce que interfaces. Bien que beaucoup d'entre eux fonctionnent avec des classes abstraites, ce n'est pas toujours une donnée et je suis très partisan de suivre le même chemin que les autres 99% des utilisateurs d'une bibliothèque lorsque cela est possible.

Raisons contre:

  • À strictement parler (comme vous l'avez souligné), vous n'avez pas vraiment besoin de
  • Si le class/ interfacechange, il y a un petit surcoût supplémentaire

Tout bien considéré, je dirais que vous devez créer le interface. Les raisons contre sont assez négligeables mais, bien qu'il soit possible d'utiliser des résumés dans la simulation et l'IoC, c'est souvent beaucoup plus facile avec les interfaces. En fin de compte, je ne critiquerais pas le code d'un collègue s'il allait dans l'autre sens.

Liath
la source