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 une 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?
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.
BaseWorker
remplit cette condition. Ce n'est pas parce que vous ne pouvez pas instancier directement un objet BaseWorker
que vous ne pouvez pas avoir un BaseWorker
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. À moins que d'autres classes implémentant IFooWorker
ne dérivent pas de BaseWorker
, vous n'avez absolument pas 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 difficile à 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 le manque d'un endroit sans ambiguïté 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.
Je dois ê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 que 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.
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, ni "Tous les BaseWorkers 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), 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 désormais une véritable 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 de IFooWorker) n'aura pas à faire la différence.
Je dois ajouter quelque chose d'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 doivent être couplés, mais cela peut ne pas être vrai dans tous les cas.
Prenez les classes de framework de collection Java:
abstract class AbstractList
class LinkedList extends AbstractList implements Queue
Le fait que le contrat Queue est implémenté par LinkedList n'a pas poussé le problème dans 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.
Je poserais la question, que se passe-t-il lorsque vous modifiez IFooWorker
, comme l'ajout d'une nouvelle méthode?
Si BaseWorker
implémente l'interface, par défaut elle 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 pour la nouvelle méthode dans la classe de base sans toucher aux classes dérivées.
Je suis encore à des années de saisir pleinement la distinction entre une classe abstraite et une interface. Chaque fois que je pense avoir une idée des concepts de base, je vais chercher sur stackexchange et j'ai 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 n'est pas la chose 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 yacht Playboy 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. Les exemples classiques Fruit-> Apple-> Fuji ou Shape-> Triangle-> Héritage équilatéral 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 similaire de manière fiable.
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.
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'entretien 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 disponible. 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.
Il y a un autre aspect à pourquoi le DbWorker
devrait implémenter IFooWorker
, 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 que la DbWorker
est réputée ne pas étendre la classe abstraite BaseWorker
, DbWorker
perd la IFooWorker
interface. Cela pourrait, mais pas nécessairement, avoir un impact sur les clients qui le consomment, s'ils attendent l'interface IFooWorker
.