Mon collègue et moi avons des opinions différentes sur la relation entre les classes de base et les interfaces. Je pense qu'une classe ne devrait pas implémenter une interface à moins que cette classe puisse être utilisée lorsqu'une implémentation de l'interface est requise. En d'autres termes, j'aime voir du code comme celui-ci:
interface IFooWorker { void Work(); }
abstract class BaseWorker {
... base class behaviors ...
public abstract void Work() { }
protected string CleanData(string data) { ... }
}
class DbWorker : BaseWorker, IFooWorker {
public void Work() {
Repository.AddCleanData(base.CleanData(UI.GetDirtyData()));
}
}
Le DbWorker est ce qui obtient l'interface IFooWorker, car c'est une implémentation instanciable de l'interface. Il remplit complètement le contrat. Mon collègue préfère le presque identique:
interface IFooWorker { void Work(); }
abstract class BaseWorker : IFooWorker {
... base class behaviors ...
public abstract void Work() { }
protected string CleanData(string data) { ... }
}
class DbWorker : BaseWorker {
public void Work() {
Repository.AddCleanData(base.CleanData(UI.GetDirtyData()));
}
}
Où la classe de base obtient l'interface, et en vertu de cela, tous les héritiers de la classe de base appartiennent également à cette interface. Cela me dérange mais je ne peux pas trouver de raisons concrètes pour lesquelles, en dehors de "la classe de base ne peut pas se suffire à elle-même comme implémentation de l'interface".
Quels sont les avantages et les inconvénients de sa méthode par rapport à la mienne, et pourquoi devrait-on l’utiliser par rapport à l’autre?
la source
Work
est le problème d'héritage du diamant (dans ce cas, ils remplissent tous les deux un contrat sur laWork
méthode). En Java, vous devez implémenter les méthodes de l'interface afin d'Work
éviter de cette façon la méthode à utiliser par le programme. Cependant, des langages tels que C ++ ne la désambiguïsent pas pour vous.Réponses:
BaseWorker
remplit cette condition. Ce n'est pas parce que vous ne pouvez pas instancier directement unBaseWorker
objet que vous ne pouvez pas avoir unBaseWorker
pointeur qui remplit le contrat. En effet, c'est à peu près tout l'intérêt des classes abstraites.En outre, il est difficile de le distinguer de l'exemple simplifié que vous avez publié, mais une partie du problème peut être que l'interface et la classe abstraite sont redondantes. Sauf si vous avez d'autres classes implémentées
IFooWorker
qui ne dérivent pasBaseWorker
, vous n'avez pas du tout besoin de l'interface. Les interfaces ne sont que des classes abstraites avec quelques limitations supplémentaires qui facilitent l'héritage multiple.Encore une fois, il est difficile de dire à partir d'un exemple simplifié, l'utilisation d'une méthode protégée, faisant explicitement référence à la base d'une classe dérivée, et l'absence d'un endroit non ambigu pour déclarer l'implémentation de l'interface sont des signes d'avertissement que vous utilisez de manière inappropriée l'héritage au lieu de composition. Sans cet héritage, toute votre question devient théorique.
la source
BaseWorker
n'est pas instanciable directement ne signifie pas qu'un élément donnéBaseWorker myWorker
n'est pas implémentéIFooWorker
.Je devrais être d'accord avec votre collègue.
Dans les deux exemples que vous donnez, BaseWorker définit la méthode abstraite Work (), ce qui signifie que toutes les sous-classes sont capables de respecter le contrat IFooWorker. Dans ce cas, je pense que BaseWorker devrait implémenter l'interface, et cette implémentation serait héritée par ses sous-classes. Cela vous évitera d'avoir à indiquer explicitement que chaque sous-classe est en effet un IFooWorker (le principe DRY).
Si vous ne définissiez pas Work () comme méthode de BaseWorker, ou si IFooWorker avait d'autres méthodes dont les sous-classes de BaseWorker ne voudraient pas ou n'auraient pas besoin, alors (évidemment) vous devrez indiquer quelles sous-classes implémentent réellement IFooWorker.
la source
IFoo
et un,BaseFoo
cela implique que ceIFoo
n'est pas une interface à granularité appropriée, ou quiBaseFoo
utilise l'héritage où la composition pourrait être un meilleur choix.MyDaoFoo
ouSqlFoo
ouHibernateFoo
ou quoi que ce soit, pour indiquer qu'il ne s'agit que d'un arbre possible de classes à implémenterIFoo
? C'est encore plus une odeur pour moi si une classe de base est couplée à une bibliothèque ou une plate-forme spécifique et il n'y a aucune mention de cela dans le nom ...Je suis généralement d'accord avec votre collègue.
Prenons votre modèle: l'interface n'est implémentée que par les classes enfants, même si la classe de base applique également l'exposition des méthodes IFooWorker. Tout d'abord, c'est redondant; que la classe enfant implémente ou non l'interface, ils sont tenus de remplacer les méthodes exposées de BaseWorker, et de même toute implémentation IFooWorker doit exposer la fonctionnalité, qu'elle obtienne ou non de l'aide de BaseWorker.
Cela rend également votre hiérarchie ambiguë; "Tous les IFooWorkers sont des BaseWorkers" n'est pas nécessairement une affirmation vraie, pas plus que "Tous les BaseWorkers ne sont des IFooWorkers". Donc, si vous souhaitez définir une variable d'instance, un paramètre ou un type de retour qui pourrait être n'importe quelle implémentation d'IFooWorker ou de BaseWorker (en tirant parti de la fonctionnalité exposée commune qui est l'une des raisons d'hériter en premier lieu), aucune des deux ceux-ci sont garantis de manière globale; certains BaseWorkers ne seront pas assignables à une variable de type IFooWorker, et vice versa.
Le modèle de votre collègue est beaucoup plus facile à utiliser et à remplacer. "Tous les BaseWorkers sont des IFooWorkers" est maintenant une vraie déclaration; vous pouvez donner n'importe quelle instance BaseWorker à n'importe quelle dépendance IFooWorker, aucun problème. L'instruction opposée "Tous les IFooWorkers sont des BaseWorkers" n'est pas vraie; qui vous permet de remplacer BaseWorker par BetterBaseWorker et votre code consommateur (qui dépend d'IFooWorker) n'aura pas à faire la différence.
la source
Je dois ajouter un avertissement à ces réponses. Le couplage de la classe de base à l'interface crée une force dans la structure de cette classe. Dans votre exemple de base, il est évident que les deux devraient être couplés, mais cela peut ne pas être vrai dans tous les cas.
Prenez les classes de framework de collection Java:
Le fait que le contrat de file d'attente soit mis en œuvre par LinkedList n'a pas poussé le problème à AbstractList .
Quelle est la distinction entre les deux cas? Le but de BaseWorker était toujours (comme indiqué par son nom et son interface) d'implémenter des opérations dans IWorker . Le but de AbstractList et celui de Queue sont divergents, mais un descendant du premier peut toujours mettre en œuvre le dernier contrat.
la source
Je poserais la question, que se passe-t-il lorsque vous changez
IFooWorker
, comme l'ajout d'une nouvelle méthode?Si
BaseWorker
implémente l'interface, par défaut il devra déclarer la nouvelle méthode, même si elle est abstraite. En revanche, s'il n'implémente pas l'interface, vous n'obtiendrez que des erreurs de compilation sur les classes dérivées.Pour cette raison, je ferais en sorte que la classe de base implémente l'interface , car je pourrais être en mesure d'implémenter toutes les fonctionnalités de la nouvelle méthode dans la classe de base sans toucher aux classes dérivées.
la source
Pensez d'abord à ce qu'est une classe de base abstraite et à ce qu'est une interface. Considérez quand vous utiliseriez l'un ou l'autre et quand vous ne l'utiliseriez pas.
Il est courant pour les gens de penser que les deux sont des concepts très similaires, en fait, c'est une question d'entrevue courante (la différence entre les deux est ??)
Donc, une classe de base abstraite vous donne quelque chose d'interfaces qui ne peut pas être l'implémentation par défaut des méthodes. (Il y a d'autres choses, en C #, vous pouvez avoir des méthodes statiques sur une classe abstraite, pas sur une interface par exemple).
Par exemple, une utilisation courante de ceci est avec IDisposable. La classe abstraite implémente la méthode Dispose d'IDisposable, ce qui signifie que toute version dérivée de la classe de base sera automatiquement jetable. Vous pouvez ensuite jouer avec plusieurs options. Make Dispose abstract, forçant les classes dérivées à l'implémenter. Fournissez une implémentation virtuelle par défaut, pour qu'ils ne le fassent pas, ou ne la rendez ni virtuelle ni abstraite et demandez-lui d'appeler des méthodes virtuelles appelées des choses comme BeforeDispose, AfterDispose, OnDispose ou Disposing par exemple.
Donc, à chaque fois que toutes les classes dérivées doivent prendre en charge l'interface, cela va sur la classe de base. Si seulement un ou certains ont besoin de cette interface, cela ira dans la classe dérivée.
C'est en fait une grossière simplification. Une autre alternative est d'avoir des classes dérivées qui n'implémentent même pas les interfaces, mais les fournissent via un modèle d'adaptateur. Un exemple de cela que j'ai vu récemment était dans IObjectContextAdapter.
la source
Je suis encore à des années de saisir pleinement la distinction entre une classe abstraite et une interface. Chaque fois que je pense maîtriser les concepts de base, je vais chercher sur stackexchange et je suis à deux pas en arrière. Mais quelques réflexions sur le sujet et la question des PO:
Première:
Il existe deux explications courantes d'une interface:
Une interface est une liste de méthodes et de propriétés que n'importe quelle classe peut implémenter, et en implémentant une interface, une classe garantit que ces méthodes (et leurs signatures) et ces propriétés (et leurs types) seront disponibles lors de "l'interfaçage" avec cette classe ou un objet de cette classe. Une interface est un contrat.
Les interfaces sont des classes abstraites qui ne font / ne peuvent rien faire. Ils sont utiles car vous pouvez en implémenter plusieurs, contrairement à ces classes parentales moyennes. Par exemple, je pourrais être un objet de la classe BananaBread et hériter de BaseBread, mais cela ne signifie pas que je ne peux pas également implémenter les interfaces IWithNuts et ITastesYummy. Je pourrais même implémenter l'interface IDoesTheDishes, parce que je ne suis pas seulement du pain, vous savez?
Il existe deux explications courantes d'une classe abstraite:
Une classe abstraite est, yknow, la chose pas ce que la chose peut faire. C'est comme, l'essence, pas vraiment une chose réelle du tout. Attendez, cela vous aidera. Un bateau est une classe abstraite, mais un Playboy Yacht sexy serait une sous-classe de BaseBoat.
J'ai lu un livre sur les classes abstraites, et peut-être devriez-vous lire ce livre, parce que vous ne le comprenez probablement pas et le ferez mal si vous ne l'avez pas lu.
Soit dit en passant, le livre Citers semble toujours impressionnant, même si je m'en vais encore confus.
Seconde:
Sur SO, quelqu'un a demandé une version plus simple de cette question, le classique, "pourquoi utiliser des interfaces? Quelle est la différence? Qu'est-ce que je manque?" Et une réponse a utilisé un pilote de l'armée de l'air comme exemple simple. Il n'a pas tout à fait atterri, mais il a suscité de grands commentaires, dont l'un a mentionné l'interface IFlyable ayant des méthodes comme takeOff, pilotEject, etc. Et cela a vraiment cliqué pour moi comme un exemple concret de la raison pour laquelle les interfaces ne sont pas seulement utiles, mais crucial. Une interface rend un objet / classe intuitif, ou du moins donne le sentiment qu'il l'est. Une interface n'est pas au profit de l'objet ou des données, mais pour quelque chose qui doit interagir avec cet objet. Le classique Fruit-> Apple-> Fuji ou Shape-> Triangle-> Les exemples équilatéraux d'héritage sont un excellent modèle pour comprendre taxonomiquement un objet donné en fonction de ses descendants. Il informe le consommateur et les processeurs de ses qualités générales, de ses comportements, que l'objet soit un groupe de choses, arrosera votre système si vous le placez au mauvais endroit, ou décrit des données sensorielles, un connecteur vers un magasin de données spécifique, ou données financières nécessaires pour faire la paie.
Un objet Plane spécifique peut ou non avoir une méthode pour un atterrissage d'urgence, mais je vais être énervé si j'assume son emergencyLand comme tout autre objet volant uniquement pour se réveiller couvert dans DumbPlane et apprendre que les développeurs sont allés avec quickLand parce qu'ils pensaient que c'était pareil. Tout comme je serais frustré si chaque fabricant de vis avait sa propre interprétation de Righty tighty ou si mon téléviseur n'avait pas le bouton d'augmentation du volume au-dessus du bouton de diminution du volume.
Les classes abstraites sont le modèle qui établit ce que tout objet descendant doit posséder pour être qualifié de classe. Si vous n'avez pas toutes les qualités d'un canard, peu importe que vous ayez l'interface IQuack implémentée, votre juste un pingouin bizarre. Les interfaces sont des choses qui ont du sens même lorsque vous ne pouvez être sûr de rien d'autre. Jeff Goldblum et Starbuck étaient tous deux capables de piloter des vaisseaux spatiaux extraterrestres parce que l'interface était fiable de manière similaire.
Troisième:
Je suis d'accord avec votre collègue, car parfois vous devez appliquer certaines méthodes au tout début. Si vous créez un ORM d'enregistrement actif, il a besoin d'une méthode de sauvegarde. Ce n'est pas à la hauteur de la sous-classe qui peut être instanciée. Et si l'interface ICRUD est suffisamment portable pour ne pas être exclusivement couplée à la seule classe abstraite, elle peut être implémentée par d'autres classes pour les rendre fiables et intuitives pour toute personne déjà familière avec l'une des classes descendantes de cette classe abstraite.
D'un autre côté, il y avait un excellent exemple plus tôt de ne pas sauter pour lier une interface à la classe abstraite, car tous les types de liste n'implémenteront pas (ou ne devraient pas) implémenter une interface de file d'attente. Vous avez dit que ce scénario se produit la moitié du temps, ce qui signifie que vous et votre collègue avez tous les deux tort la moitié du temps, et donc la meilleure chose à faire est d'argumenter, de débattre, de considérer, et s'ils s'avèrent corrects, reconnaissez et acceptez le couplage . Mais ne devenez pas un développeur qui suit une philosophie même si elle n'est pas la meilleure pour le travail à accomplir.
la source
Il y a un autre aspect à la raison pour laquelle la
DbWorker
mise en œuvre devraitIFooWorker
, comme vous le suggérez.Dans le cas du style de votre collègue, si, pour une raison quelconque, une refactorisation ultérieure se produit et
DbWorker
est réputée ne pas étendre laBaseWorker
classe abstraite ,DbWorker
l'IFooWorker
interface perd . Cela pourrait, mais pas nécessairement, avoir un impact sur les clients qui le consomment, s'ils attendent l'IFooWorker
interface.la source
Pour commencer, j'aime définir différemment les interfaces et les classes abstraites:
Dans votre cas, tous les travailleurs mettent en œuvre
work()
parce que c'est ce que le contrat exige d'eux. Par conséquent, votre classe de baseBaseworker
doit implémenter cette méthode de manière explicite ou en tant que fonction virtuelle. Je vous suggère de mettre cette méthode dans votre classe de base en raison du simple fait que tous les travailleurs doivent le fairework()
. Par conséquent, il est logique que votre classe de base (même si vous ne pouvez pas créer un pointeur de ce type) l'inclue en tant que fonction.Maintenant,
DbWorker
satisfait l'IFooWorker
interface, mais s'il existe un moyen spécifique pour cette classework()
, alors elle a vraiment besoin de surcharger la définition héritée deBaseWorker
. La raison pour laquelle il ne devrait pas implémenter l'interfaceIFooWorker
directement est que ce n'est pas quelque chose de spécialDbWorker
. Si vous le faisiez à chaque fois que vous implémentiez des classes similaires,DbWorker
vous violeriez DRY .Si deux classes implémentent la même fonction de différentes manières, vous pouvez commencer à rechercher la plus grande superclasse commune. La plupart du temps, vous en trouverez un. Sinon, continuez à chercher ou abandonnez et acceptez qu'ils n'ont pas suffisamment de choses en commun pour faire une classe de base.
la source
Wow, toutes ces réponses et aucune ne soulignant que l'interface dans l'exemple ne sert à rien. Vous n'utilisez pas l'héritage et une interface pour la même méthode, c'est inutile. Vous utilisez l'un ou l'autre. Il y a beaucoup de questions sur ce forum avec des réponses expliquant les avantages de chacun et les sénarios qui appellent l'un ou l'autre.
la source