J'ai un certain nombre de classes Processor
qui feront deux choses très différentes, mais sont appelées à partir du code commun (une situation "d'inversion de contrôle").
Je me demande de quelles considérations de conception je devrais être conscient (ou conscient, pour vous, les utilisateurs) de décider s'ils devraient tous hériter de BaseProcessor
, ou implémenter IProcessor
comme interface.
En règle générale, la règle se présente comme suit:
Pour mettre cela en termes un peu plus concrets, regardons un exemple. Le System.Drawing.Bitmap
classe est une image (et en tant que telle, elle hérite de la classe Image
), mais elle aussi can-do éliminant, donc il implémente l'interface IDisposable
. Il permet également la sérialisation , il implémente donc depuis l'interface ISerializable
.
Mais plus concrètement, les interfaces sont souvent utilisées pour simuler l'héritage multiple en C #. Si votre classe Processor
doit hériter de quelque chose comme System.ComponentModel.Component
, alors vous n'avez d'autre choix que d'implémenter une interface IProcessor
.
Le fait est que les interfaces et la classe de base abstraite fournissent un contrat spécifiant ce qu'une classe particulière peut faire. C'est un mythe commun que les interfaces sont nécessaires pour déclarer ce contrat, mais ce n'est pas correct. Le plus grand avantage pour moi est que les classes de base abstraites vous permettent de fournir des fonctionnalités par défaut pour les sous-classes. Mais si aucune fonctionnalité par défaut n'a de sens, rien ne vous empêche de marquer la méthode elle-même comme abstract
, ce qui nécessite que les classes dérivées l'implémentent elles-mêmes, comme si elles devaient implémenter une interface.
Pour des réponses à des questions comme celle-ci, je me tourne souvent vers les . NET Framework Design Guidelines , qui ont ceci à dire sur le choix entre les classes et les interfaces:
En général, les classes sont la construction préférée pour exposer les abstractions.
Le principal inconvénient des interfaces est qu'elles sont beaucoup moins flexibles que les classes quand il s'agit de permettre l'évolution des API. Une fois que vous expédiez une interface, l'ensemble de ses membres est fixé pour toujours. Tout ajout à l'interface romprait les types existants implémentant l'interface.
Une classe offre beaucoup plus de flexibilité. Vous pouvez ajouter des membres aux classes que vous avez déjà expédiées. Tant que la méthode n'est pas abstraite (c'est-à-dire tant que vous fournissez une implémentation par défaut de la méthode), toutes les classes dérivées existantes continuent de fonctionner inchangées.
[. . . ]
L'un des arguments les plus courants en faveur des interfaces est qu'elles permettent de séparer le contrat de l'implémentation. Cependant, l'argument suppose à tort que vous ne pouvez pas séparer les contrats de l'implémentation à l'aide de classes. Les classes abstraites résidant dans un assemblage distinct de leurs implémentations concrètes sont un excellent moyen de réaliser une telle séparation.
Leurs recommandations générales sont les suivantes:
Chris Anderson exprime un accord particulier avec ce dernier principe, faisant valoir que:
Les types abstraits améliorent la version et permettent une extensibilité future, mais ils brûlent également votre seul et unique type de base. Les interfaces sont appropriées lorsque vous définissez réellement un contrat entre deux objets qui est invariant dans le temps. Les types de base abstraits conviennent mieux pour définir une base commune pour une famille de types.
Richard,
Pourquoi choisir entre eux? J'aurais une interface IProcessor comme le type publié (pour une utilisation ailleurs dans le système); et s'il se trouve que vos différentes implémentations CURRENT d'IProcessor ont un comportement commun, alors une classe abstraite BaseProcessor serait un très bon endroit pour implémenter ce comportement commun.
De cette façon, si vous avez besoin à l'avenir d'un IProcessor qui n'a PAS été pour les services de BaseProcessor, il n'a PAS à l'avoir (et peut-être à le cacher) ... mais ceux qui le veulent peuvent l'avoir ... en code/concepts dupliqués.
Juste mon humble AVIS.
À votre santé. Keith.
Les interfaces sont un "contrat", elles garantissent que certaines classes implémentent un ensemble souhaité de membres - propriétés, méthodes et événements -.
Les classes de base (concrètes ou abstraites, peu importe) sont l'archétype d'une entité. Ce sont des entités représentant ce qui est commun dans une réelle physique ou conceptuelle.
Quand utiliser les interfaces?
Chaque fois qu'un type doit déclarer qu'il a, au moins, certains comportements et propriétés qu'un consommateur devrait prendre en compte et les utiliser pour accomplir une tâche.
Quand utiliser les classes de base (concrètes et/ou abstraites)
Chaque fois qu'un groupe d'entités partage le même archétype, ce qui signifie que B hérite de A parce que B est A avec des différences, mais B peut être identifié comme A.
Exemples:
Parlons des tables.
Nous acceptons les tables recyclables => Cela doit être défini avec une interface comme "IRecyclableTable" garantissant que toutes les tables recyclables auront une méthode "Recycle" .
Nous voulons des tables de burea => Ceci doit être défini avec héritage. Une "table de bureau" est une "table". Toutes les tables ont des propriétés et des comportements communs et celles du bureau auront les mêmes en ajoutant des éléments qui font que la table du bureau fonctionne différemment des autres types de tables.
Je pourrais parler d'associations, sens des deux cas dans un graphe objet, mais à mon humble avis, si j'ai besoin de donner des arguments d'un point de vue conceptuel, je répondrais exactement avec cette argumentation.
Je ne suis pas assez bon dans les choix de conception, mais si on me le demande, je préférerai implémenter une interface iProcessor s'il n'y a que des membres à étendre. S'il y a d'autres fonctions qui n'ont pas besoin d'être étendues, hériter du processeur de base est une meilleure option.
La pureté de conception mise à part, vous ne pouvez hériter qu'une seule fois, donc si vous avez besoin d'hériter d'une classe de framework, la question est théorique.
Si vous avez le choix, vous pouvez pragmatiquement choisir l'option qui enregistre le plus de frappe. C'est généralement le choix puriste de toute façon.
MODIFIER:
Si la classe de base aura une implémentation, cela pourrait être utile, si elle est purement abstraite, elle pourrait aussi bien être une interface.
Si ce que vous choisissez n'a pas d'importance, choisissez toujours Interface. Il permet plus de flexibilité. Cela pourrait vous protéger contre de futurs changements (si vous devez changer quelque chose dans la classe de base, les classes héritées peuvent être affectées). Il permet également de mieux encapsuler les détails. Si vous utilisez une inversion de contrôle de l'injection de dépendance, ils ont tendance à favoriser l'interface.
Ou si l'héritage ne peut être évité, c'est probablement une bonne idée de les utiliser ensemble. Créez une classe de base abstraite qui implémente une interface. Dans votre cas, ProcessorBase implémente un IProcessor.
Similaire à ControllerBase et IController dans ASP.NET Mvc.
Étant donné que les principes SOLID offrent plus de maintenabilité et d'extensibilité à votre projet, je préférerais les interfaces à l'héritage.
De plus, si vous devez ajouter des "fonctionnalités supplémentaires" à votre interface, la meilleure option est de créer une nouvelle interface, en suivant le I dans SOLID, qui est le principe de ségrégation d'interface.