Quand devrais-je choisir l’inheritance sur une interface lors de la conception des bibliothèques de classes C #?

J’ai un nombre de classes Processor qui vont faire deux choses très différentes, mais qui sont appelées à partir du code commun (une situation “d’inversion de contrôle”).

Je me demande quelles considérations de conception je devrais connaître (ou être conscient, pour vous, les utilisateurs) lorsque vous décidez s’ils doivent tous hériter de BaseProcessor , ou implémenter IProcessor comme interface.

En règle générale, la règle va quelque chose comme ceci:

  • L’inheritance décrit une relation is-a .
  • L’implémentation d’une interface décrit une relation de proximité.

En termes plus concrets, examinons un exemple. La classe System.Drawing.Bitmap est une image (et en tant que telle, elle hérite de la classe Image ), mais elle est également capable de disposer, de sorte qu’elle implémente l’interface IDisposable . Il peut aussi faire de la sérialisation, donc il implémente depuis l’interface ISerializable .

Mais de manière plus pratique, les interfaces sont souvent utilisées pour simuler l’inheritance multiple en C #. Si votre classe de Processor doit hériter de quelque chose comme System.ComponentModel.Component , alors vous n’avez pas d’autre choix que d’implémenter une interface IProcessor .

Le fait est que les deux interfaces et la classe de base abstraite fournissent un contrat spécifiant ce que peut faire une classe particulière. C’est un mythe courant 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 s’il n’y a pas de fonctionnalité par défaut qui soit logique, rien ne vous empêche de marquer la méthode elle-même comme abstract , exigeant que les classes dérivées l’implémentent elles-mêmes, comme si elles implémentaient une interface.

Pour des réponses à des questions comme celle-ci, je me tourne souvent vers les directives de conception .NET Framework , qui expliquent 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 en ce qui concerne l’évolution des API. Une fois que vous expédiez une interface, le jeu de ses membres est fixe pour toujours. Tout ajout à l’interface briserait les types existants implémentant l’interface.

Une classe offre beaucoup plus de flexibilité. Vous pouvez append des membres aux classes que vous avez déjà expédiées. Tant que la méthode n’est pas abstraite (c.-à-d. Tant que vous fournissez une implémentation par défaut de la méthode), toutes les classes dérivées existantes continuent de fonctionner sans modification.

[. . . ]

L’un des arguments les plus courants en faveur des interfaces est qu’ils 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 constituent un excellent moyen de parvenir à une telle séparation.

Leurs recommandations générales sont les suivantes:

  • Ne privilégiez pas la définition des classes sur les interfaces.
  • Utilisez des classes abstraites plutôt que des interfaces pour découpler le contrat des implémentations. Les classes abstraites, si elles sont définies correctement, permettent d’obtenir le même degré de découplage entre le contrat et la mise en œuvre.
  • Définissez une interface si vous devez fournir une hiérarchie polymorphe de types de valeur.
  • Envisagez de définir des interfaces pour obtenir un effet similaire à celui de l’inheritance multiple.

Chris Anderson exprime un accord particulier avec ce dernier principe, arguant que:

Les types abstraits font beaucoup mieux la version et permettent une extensibilité future, mais ils gravent également votre seul et unique type de base. Les interfaces sont appropriées lorsque vous définissez réellement un contrat entre deux objects invariant dans le temps. Les types de base abstraits sont meilleurs pour définir une base commune pour une famille de types.

Richard,

Pourquoi choisir entre eux? J’aurais une interface IProcesseur comme type publié (pour une utilisation ailleurs dans le système); et s’il se trouve que vos diverses implémentations CURRENT d’IProcessor ont un comportement commun, alors une classe abstraite BaseProcessor serait un bon endroit pour implémenter ce comportement commun.

De cette façon, si vous avez besoin d’un IProcessor à l’avenir qui ne soit pas destiné aux services de BaseProcessor, il NE DOIT PAS l’avoir (et peut-être le cacher) … mais ceux qui le désirent peuvent l’avoir … en code / concepts dupliqués.

Juste mon humble avis.

À votre santé. Keith

Les interfaces sont un “contrat”, elles garantissent qu’une classe implémente un ensemble 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 forme physique ou conceptuelle réelle.

Quand utiliser les interfaces?

Chaque fois qu’un type a besoin de déclarer cela, au moins, certains comportements et propriétés doivent être pris en compte par un consommateur et utilisés pour accomplir certaines tâches.

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, cela signifie que B hérite de A car B est A avec des différences, mais que B peut être identifié comme A.


Exemples:

Parlons des tables.

  • Nous acceptons les tables recyclables => Ceci 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 bureau => Ceci doit être défini avec l’inheritance. Une “table de bureau” est une “table”. Toutes les tables ont des propriétés et des comportements communs, et ceux du bureau auront les mêmes caractéristiques, ce qui fait qu’une table de bureau fonctionne différemment des autres types de tables.

Je pourrais parler d’associations, de sens des deux cas dans un graphe d’objects, mais à mon avis, si je dois donner des arguments d’un sharepoint vue conceptuel, je répondrais exactement par cette argumentation.

Je ne suis pas très bon en matière de choix de conception, mais si on me le demande, je préférerais mettre en place 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, l’inheritance de baseprocessor est une meilleure option.

La pureté de conception mise à part, vous ne pouvez hériter qu’une seule fois, donc si vous devez hériter d’une classe de framework, la question est sans object.

Si vous avez le choix, de manière pragmatique, vous pouvez choisir l’option qui enregistre le plus de frappe. C’est généralement le choix des puristes.

MODIFIER:

Si la classe de base a une implémentation, cela pourrait être utile, si elle est purement abstraite, elle peut aussi bien être une interface.

Si ce que vous choisissez n’a pas d’importance, choisissez toujours Interface. Cela permet plus de flexibilité. Cela pourrait vous protéger des changements futurs (si vous devez changer quelque chose dans la classe de base, les classes héritées peuvent être affectées). Cela permet aussi d’encapsuler mieux les détails. Si vous utilisez une Inversion de contrôle de l’dependency injection, ils ont tendance à favoriser l’interface.

Ou si l’inheritance ne peut pas être évité, 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 plutôt que l’inheritance.

De même, si vous avez besoin d’append des “fonctionnalités supplémentaires” à votre interface, la meilleure option consiste à créer une nouvelle interface, en suivant le I dans SOLID, qui est le principe de la séparation des interfaces.