Déclarer l'interface dans le même fichier que la classe de base, est-ce une bonne pratique?

29

Pour être interchangeables et testables, normalement les services avec logique doivent avoir une interface, par exemple

public class FooService: IFooService 
{ ... }

Côté design, je suis d'accord avec cela, mais l'une des choses qui me dérange avec cette approche est que pour un service, vous devrez déclarer deux choses (la classe et l'interface), et dans notre équipe, normalement deux fichiers (un pour la classe et un pour l'interface). Un autre inconfort est la difficulté de navigation car l'utilisation de "Aller à la définition" dans IDE (VS2010) pointera vers l'interface (puisque les autres classes se réfèrent à l'interface), pas la classe réelle.

Je pensais qu'écrire IFooService dans le même fichier que FooService réduirait l'étrangeté ci-dessus. Après tout, IFooService et FooService sont très liés. Est-ce une bonne pratique? Y a-t-il une bonne raison pour que IFooService se trouve dans son propre fichier?

Louis Rhys
la source
3
Si vous n'avez jamais qu'une seule implémentation d'une interface particulière, il n'y a pas vraiment besoin logique de l'interface. Pourquoi ne pas utiliser la classe directement?
Joris Timmermans
11
@MadKeithV pour le couplage lâche et la testabilité?
Louis Rhys
10
@MadKeithV: vous avez raison. Mais lorsque vous écrivez des tests unitaires pour le code qui repose sur IFooService, vous fournissez généralement un MockFooService, qui est une deuxième implémentation de cette interface.
Doc Brown
5
@DocBrown - si vous avez plusieurs implémentations, le commentaire disparaît évidemment, mais l'utilité de pouvoir accéder directement à l '"implémentation unique" (car il y en a au moins deux). Ensuite, la question devient: quelles informations dans l'implémentation concrète ne devraient pas déjà figurer dans la description / documentation de l'interface au point où vous utilisez l'interface?
Joris Timmermans
1
Auto-publicité flagrante: stackoverflow.com/questions/5840219/…
Marjan Venema

Réponses:

24

Il ne doit pas nécessairement se trouver dans son propre fichier, mais votre équipe doit décider d'une norme et s'y tenir.

De plus, vous avez raison de dire que "Aller à la définition" vous amène à l'interface, mais si Resharper est installé, il suffit d'un clic pour ouvrir une liste des classes / interfaces dérivées de cette interface, donc ce n'est pas un gros problème. C'est pourquoi je garde l'interface dans un fichier séparé.

 

Scott Whitlock
la source
5
Je suis d'accord sur cette réponse, après tout, l'IDE devrait s'adapter à notre conception et non l'inverse, non?
marktani
1
De plus, sans Resharper, F12 et Shift + F12 font à peu près la même chose. Pas même un clic droit :) (pour être honnête, il ne vous faudra que des utilisations directes de l'interface, pas sûr de ce que fait la version Resharper)
Daniel B
@DanielB: Dans Resharper, Alt-End va à l'implémentation (ou vous présente une liste d'implémentations possibles pour aller à).
StriplingWarrior
@StriplingWarrior, il semble que c'est exactement ce que fait également vanilla VS. F12 = aller à la définition, Shift + F12 = aller à l'implémentation (je suis sûr que cela fonctionne sans Resharper, bien que je l'ai installé, il est donc difficile de confirmer). C'est l'un des raccourcis de navigation les plus utiles, je ne fais que passer le mot.
Daniel B
@DanielB: La valeur par défaut pour Shift-F12 est "trouver les utilisations". jetbrains.com/resharper/webhelp/…
StriplingWarrior
15

Je pense que vous devriez les conserver dans des fichiers séparés. Comme vous l'avez dit, l'idée est de rester testable et interchangeable. En plaçant l'interface dans le même fichier que votre implémentation, vous associez l'interface à l'implémentation spécifique. Si vous décidez de créer un objet factice ou une autre implémentation, l'interface ne sera pas logiquement séparée de FooService.

Brad S
la source
10

Selon SOLID, non seulement vous devez créer l'interface, et non seulement si elle doit être dans un fichier différent, elle doit être dans un assemblage différent.

Pourquoi? Parce que toute modification d'un fichier source qui se compile dans un assembly nécessite une recompilation de l'assembly, et toute modification d'un assembly nécessite la recompilation de tout assembly dépendant. Donc, si votre objectif, basé sur SOLID, est de pouvoir remplacer une implémentation A par une implémentation B, alors que la classe C dépendant de l'interface I ne doit pas connaître la différence, vous devez vous assurer que l'assemblage avec I en elle ne change pas, protégeant ainsi les usages.

"Mais c'est juste une recompilation", je vous entends protester. C'est possible, mais dans votre application pour smartphone, ce qui est plus facile avec la bande passante de données de vos utilisateurs; télécharger un binaire qui a changé, ou télécharger ce binaire et cinq autres avec du code qui en dépend? Tous les programmes ne sont pas écrits pour être consommés par les ordinateurs de bureau sur un réseau local. Même dans ce cas, où la bande passante et la mémoire sont bon marché, les versions de correctifs plus petites peuvent avoir de la valeur car elles sont faciles à diffuser sur l'ensemble du LAN via Active Directory ou des couches de gestion de domaine similaires; vos utilisateurs n'attendront que quelques secondes pour qu'il soit appliqué la prochaine fois qu'ils se connectent au lieu de quelques minutes pour que le tout soit réinstallé. Sans oublier que, moins il y a d'assemblages à recompiler lors de la construction d'un projet, plus vite il se construira,

Maintenant, l'avertissement: ce n'est pas toujours possible ou faisable. La façon la plus simple de le faire est de créer un projet "d'interfaces" centralisé. Cela a ses propres inconvénients; le code devient moins réutilisable car le projet d'interface ET le projet d'implémentation doivent être référencés dans d'autres applications réutilisant la couche de persistance ou d'autres composants clés de votre application. Vous pouvez surmonter ce problème en divisant les interfaces en assemblages plus étroitement couplés, mais vous avez alors plus de projets dans votre application, ce qui rend une construction complète très douloureuse. La clé est l'équilibre et le maintien de la conception à couplage lâche; vous pouvez généralement déplacer des fichiers si nécessaire, donc quand vous voyez qu'une classe aura besoin de nombreuses modifications, ou que de nouvelles implémentations d'une interface seront nécessaires régulièrement (peut-être pour interfacer avec les versions nouvellement prises en charge d'autres logiciels,

KeithS
la source
8

Pourquoi avoir des fichiers séparés est un inconfort? Pour moi, c'est beaucoup plus propre et plus propre. Il est courant de créer un sous-dossier nommé "Interfaces" et d'y coller vos fichiers IFooServer.cs, si vous voulez voir moins de fichiers dans votre Explorateur de solutions.

La raison pour laquelle l'interface est définie dans son propre fichier est pour la même raison que les classes sont généralement définies dans leur propre fichier: la gestion de projet est plus simple lorsque votre structure logique et la structure de fichier sont identiques, de sorte que vous savez toujours quel fichier une classe donnée est définie dans. Cela peut vous faciliter la vie lors du débogage (les traces de pile d'exceptions vous donnent généralement des numéros de fichier et de ligne) ou lors de la fusion de code source dans un référentiel de contrôle de source.

Avner Shahar-Kashtan
la source
6

Il est souvent recommandé de laisser vos fichiers de code ne contenir qu'une seule classe ou une seule interface. Mais ces pratiques de codage sont un moyen d'arriver à une fin - pour mieux structurer votre code et le rendre plus facile à utiliser. Si vous, et votre équipe, trouvez plus facile de travailler avec si les classes sont maintenues avec leurs interfaces, faites-le par tous les moyens.

Personnellement, je préfère avoir l'interface et la classe dans le même fichier lorsqu'il n'y a qu'une seule classe qui implémente l'interface, comme dans votre cas.

En ce qui concerne les problèmes que vous rencontrez avec la navigation, je peux fortement recommander ReSharper . Il contient des raccourcis très utiles pour passer directement à la méthode implémentant une méthode d'interface spécifique.

Pete
la source
3
+1 pour avoir souligné que les pratiques de l'équipe devaient dicter la structure du fichier et pour avoir l'approche contraire à la norme.
3

Il y a de nombreuses fois où vous souhaitez que votre interface soit non seulement dans un fichier séparé de la classe, mais même dans un assemblage distinct.

Par exemple, une interface de contrat de service WCF peut être partagée par le client et le service si vous contrôlez les deux extrémités du câble. En déplaçant l'interface dans son propre assembly, elle aura moins de dépendances d'assembly. Cela permet au client de consommer beaucoup plus facilement, en relâchant le couplage avec sa mise en œuvre.

StuartLC
la source
2

Il est rarement, voire jamais, logique d'avoir une seule implémentation d'une interface 1 . Si vous mettez une interface publique et une classe publique implémentant cette interface dans le même fichier, il y a de fortes chances que vous n'ayez pas besoin d'une interface.

Lorsque la classe que vous colocalisez avec l'interface est abstraite et que vous savez que toutes les implémentations de votre interface doivent hériter de cette classe abstraite, localiser les deux dans le même fichier est logique. Vous devriez toujours examiner attentivement votre décision d'utiliser une interface: demandez-vous si une classe abstraite en elle-même conviendrait bien, et supprimez l'interface si la réponse est affirmative.

Cependant, en règle générale, vous devez vous en tenir à la stratégie "une classe / interface publique correspond à un fichier": elle est facile à suivre et facilite la navigation dans votre arbre source.


1 Une exception notable est lorsque vous avez besoin d'une interface à des fins de test, car votre choix de cadre de simulation a imposé cette exigence supplémentaire à votre code.

dasblinkenlight
la source
3
C'est en fait un modèle tout à fait normal d'avoir une interface pour une classe dans .NET, car cela permet aux tests unitaires de remplacer les dépendances par des mocks, des stubs, des spys ou d'autres doublons de test.
Pete
2
@Pete J'ai modifié ma réponse pour l'inclure sous forme de note. Je n'appellerais pas ce modèle "normal", car il laisse votre problème de testabilité "fuir" dans votre base de code principale. Les frameworks de moquerie modernes vous aident à surmonter ce problème dans une large mesure, mais avoir une interface simplifie définitivement les choses.
dasblinkenlight
2
@KeithS Une classe sans interface ne devrait être dans votre conception que lorsque vous êtes absolument certain que cela n'a aucun sens de la sous-classer et que vous créez cette classe sealed. Si vous pensez que vous pourrez à un moment donné à l'avenir ajouter une deuxième sous-classe, vous mettez immédiatement une interface. PS Un assistant de refactoring de qualité décente peut être le vôtre à un prix modique d'une seule licence ReSharper :)
dasblinkenlight
1
@KeithS Et oui, toutes vos classes qui ne sont pas spécifiquement conçues pour le sous-classement doivent être scellées. En fait, les classes souhaitées ont été scellées par défaut, vous obligeant à désigner explicitement les classes pour l'héritage, de la même manière que le langage vous oblige actuellement à désigner des fonctions à remplacer en les marquant virtual.
dasblinkenlight
2
@KeithS: Que se passe-t-il lorsque votre implémentation unique devient deux? Comme lorsque votre implémentation change; vous modifiez le code pour refléter ce changement. YAGNI s'applique ici. Vous ne saurez jamais ce qui va changer à l'avenir, alors faites la chose la plus simple possible. (Malheureusement, les tests ne respectent pas cette maxime)
Groky
2

Les interfaces appartiennent à leurs clients et non à leurs implémentations, telles que définies dans les principes, pratiques et modèles agiles de Robert C. Martin. Ainsi, combiner interface et implémentation au même endroit est contraire à son principe.

Le code client dépend de l'interface. Cela permet de compiler et de déployer le code client et l'interface sans l'implémentation. Vous pouvez avoir différentes implémentations fonctionnant comme des plugins pour le code client.

MISE À JOUR: Ce n'est pas un principe uniquement agile ici. Le Gang of Four in Design Patterns, en 1994, parle déjà de clients adhérant aux interfaces et programmant les interfaces. Leur point de vue est similaire.

Patkos Csaba
la source
1
L'utilisation des interfaces va bien au-delà du domaine que possède Agile. Agile est une méthodologie de développement qui utilise des constructions de langage de manière particulière. Une interface est une construction de langage.
1
Oui, mais l'interface appartient toujours au client et non à l'implémentation, indépendamment du fait que nous parlons d'agile ou non.
Patkos Csaba
Je suis avec toi là-dessus.
Marjan Venema
0

Personnellement, je n'aime pas le «je» devant une interface. En tant qu'utilisateur d'un tel type, je ne veux pas voir qu'il s'agit d'une interface ou non. Le type est la chose intéressante. En termes de dépendances, votre FooService est UNE implémentation possible d'IFooService. L'interface doit être proche de l'endroit où elle est utilisée. D'un autre côté, la mise en œuvre doit se faire à un endroit où elle peut être facilement modifiée sans affecter le client. Donc, je préférerais deux fichiers - normalement.

ollins
la source
2
En tant qu'utilisateur, je veux savoir si un type est une interface, car par exemple cela signifie que je ne peux pas l'utiliser newavec, mais d'un autre côté, je peux l'utiliser en co ou contre-variante. Et le Iest une convention de dénomination établie dans .Net, c'est donc une bonne raison d'y adhérer, ne serait-ce que pour la cohérence avec d'autres types.
svick
Commencer par Idans l'interface n'était que l'introduction à mes arguments sur la séparation dans deux fichiers. Le code est mieux lisible et rend plus claire l'inversion des dépendances.
ollins
4
Qu'on le veuille ou non, l'utilisation d'un «I» devant un nom d'interface est une norme de facto dans .NET. Ne pas suivre la norme dans un projet .NET serait à mon avis une violation du «principe du moindre étonnement».
Pete
Mon point principal est: séparé en deux fichiers. Un pour l'abstraction et un pour la mise en œuvre. Si l'utilisation du Iest normale dans votre environnement: utilisez-le! (Mais je n'aime pas ça :))
ollins
Et dans le cas de P.SE, le préfixe I devant une interface rend plus évident que l'affiche parle d'une interface, non héritée d'une autre classe.
0

Le codage des interfaces peut être mauvais, est en fait traité comme un anti-modèle pour certains contextes et n'est pas une loi. Habituellement, un bon exemple de codage des interfaces anti-modèle est lorsque vous avez une interface qui n'a qu'une seule implémentation dans votre application. Un autre serait si le fichier d'interface devient si gênant que vous devez cacher sa déclaration dans le fichier de la classe implémentée.

Shivan Dragon
la source
avoir une seule implémentation dans votre application d'une interface n'est pas nécessairement une mauvaise chose; on pourrait protéger l'application contre le changement technologique. Par exemple, une interface pour une technologie de persistance des données. Vous n'auriez qu'une seule implémentation pour la technologie actuelle de persistance des données tout en protégeant l'application si cela devait changer. Il n'y aurait donc qu'un seul lutin à ce moment-là.
TSmith
0

Mettre une classe et une interface dans le même fichier ne met aucune limite sur la façon dont l'une ou l'autre peut être utilisée. Une interface peut toujours être utilisée pour les simulations, etc. de la même manière, même si elle se trouve dans le même fichier qu'une classe qui l'implémente. La question pourrait être perçue uniquement comme une question de commodité organisationnelle, auquel cas je dirais faire tout ce qui vous facilite la vie!

Bien sûr, on pourrait faire valoir que le fait d'avoir les deux dans le même fichier pourrait conduire les imprudents à les considérer comme étant plus couplés par programme qu'ils ne le sont réellement, ce qui constitue un risque.

Personnellement, si je tombais sur une base de code qui utilisait une convention sur l'autre, je ne serais pas trop inquiet de toute façon.

Holf
la source
Ce n'est pas seulement une «commodité organisationnelle» ... c'est aussi une question de gestion des risques, de gestion du déploiement, de traçabilité du code, de contrôle des changements, de bruit de contrôle de source, de gestion de sécurité. Il y a plusieurs angles à cette question.
TSmith