Je réfléchis à la conception d'une bibliothèque C #, qui aura plusieurs fonctions différentes de haut niveau. Bien sûr, ces fonctions de haut niveau seront implémentées autant que possible en utilisant les principes de conception de classe SOLID . En tant que tel, il y aura probablement des classes destinées aux consommateurs à utiliser directement et régulièrement, et des "classes de support" qui sont des dépendances de ces classes "d'utilisateur final" plus courantes.
La question est de savoir quelle est la meilleure façon de concevoir la bibliothèque:
- DI Agnostic - Bien que l'ajout d'un "support" de base pour une ou deux des bibliothèques DI courantes (StructureMap, Ninject, etc.) semble raisonnable, je veux que les consommateurs puissent utiliser la bibliothèque avec n'importe quel framework DI.
- Non DI utilisable - Si un consommateur de la bibliothèque n'utilise pas de DI, la bibliothèque doit toujours être aussi facile à utiliser que possible, réduisant la quantité de travail qu'un utilisateur doit faire pour créer toutes ces dépendances "sans importance" juste pour accéder à les "vraies" classes qu'ils veulent utiliser.
Ma pensée actuelle est de fournir quelques "modules d'enregistrement DI" pour les bibliothèques DI courantes (par exemple un registre StructureMap, un module Ninject), et un ensemble ou des classes Factory qui ne sont pas DI et contiennent le couplage à ces quelques usines.
Pensées?
Réponses:
C'est en fait simple à faire une fois que vous comprenez que l'ID concerne les modèles et les principes , pas la technologie.
Pour concevoir l'API de manière agnostique au conteneur DI, suivez ces principes généraux:
Programmer vers une interface, pas une implémentation
Ce principe est en fait une citation (de mémoire cependant) de Design Patterns , mais il devrait toujours être votre véritable objectif . L'ID n'est qu'un moyen d'atteindre cet objectif .
Appliquer le principe d'Hollywood
Le principe d'Hollywood en termes de DI dit: N'appelez pas le conteneur DI, il vous appellera .
Ne demandez jamais directement une dépendance en appelant un conteneur depuis votre code. Demandez-le implicitement en utilisant l' injection de constructeur .
Utiliser l'injection de constructeur
Lorsque vous avez besoin d'une dépendance, demandez-la de manière statique via le constructeur:
Remarquez comment la classe Service garantit ses invariants. Une fois qu'une instance est créée, la dépendance est garantie d'être disponible en raison de la combinaison de la clause Guard et du
readonly
mot clé.Utilisez Abstract Factory si vous avez besoin d'un objet éphémère
Les dépendances injectées avec Constructor Injection ont généralement une longue durée de vie, mais parfois vous avez besoin d'un objet à courte durée de vie, ou pour construire la dépendance en fonction d'une valeur connue uniquement au moment de l'exécution.
Voir ceci pour plus d'informations.
Composez uniquement au dernier moment responsable
Gardez les objets découplés jusqu'à la fin. Normalement, vous pouvez attendre et tout câbler dans le point d'entrée de l'application. C'est ce qu'on appelle la racine de composition .
Plus de détails ici:
Simplifiez l'utilisation d'une façade
Si vous pensez que l'API résultante devient trop complexe pour les utilisateurs novices, vous pouvez toujours fournir quelques classes Facade qui encapsulent les combinaisons de dépendances courantes.
Pour fournir une façade flexible avec un haut degré de découverte, vous pouvez envisager de fournir des constructeurs fluides. Quelque chose comme ça:
Cela permettrait à un utilisateur de créer un Foo par défaut en écrivant
Il serait cependant très découvrable qu'il est possible de fournir une dépendance personnalisée, et vous pourriez écrire
Si vous imaginez que la classe MyFacade encapsule de nombreuses dépendances différentes, j'espère qu'il est clair comment elle fournirait les valeurs par défaut appropriées tout en rendant l'extensibilité détectable.
FWIW, longtemps après avoir écrit cette réponse, j'ai développé les concepts ici et écrit un article de blog plus long sur les bibliothèques DI-Friendly , et un article compagnon sur les cadres DI-Friendly .
la source
Le terme «injection de dépendance» n'a rien à voir spécifiquement avec un conteneur IoC, même si vous avez tendance à les voir mentionnés ensemble. Cela signifie simplement qu'au lieu d'écrire votre code comme ceci:
Vous l'écrivez comme ceci:
Autrement dit, vous faites deux choses lorsque vous écrivez votre code:
Fiez-vous aux interfaces plutôt qu'aux classes chaque fois que vous pensez que l'implémentation doit être modifiée;
Au lieu de créer des instances de ces interfaces à l'intérieur d'une classe, passez-les en tant qu'arguments de constructeur (alternativement, elles pourraient être affectées à des propriétés publiques; la première est l' injection de constructeur , la seconde est l' injection de propriété ).
Rien de tout cela ne présuppose l'existence d'une bibliothèque DI, et cela ne rend pas vraiment le code plus difficile à écrire sans une.
Si vous cherchez un exemple de cela, ne cherchez pas plus loin que le .NET Framework lui-même:
List<T>
met en œuvreIList<T>
. Si vous concevez votre classe à utiliserIList<T>
(ouIEnumerable<T>
), vous pouvez tirer parti de concepts tels que le chargement différé, comme Linq to SQL, Linq to Entities et NHibernate le font tous en arrière-plan, généralement via l'injection de propriétés. Certaines classes de framework acceptentIList<T>
en fait un argument comme constructeur, tel queBindingList<T>
, qui est utilisé pour plusieurs fonctionnalités de liaison de données.Linq to SQL et EF sont entièrement construits autour
IDbConnection
des interfaces et connexes, qui peuvent être transmises via les constructeurs publics. Vous n'avez cependant pas besoin de les utiliser; les constructeurs par défaut fonctionnent très bien avec une chaîne de connexion située quelque part dans un fichier de configuration.Si vous travaillez sur des composants WinForms, vous traitez avec des "services", comme
INameCreationService
ouIExtenderProviderService
. Vous ne savez même pas vraiment quelles sont les classes concrètes . .NET a en fait son propre conteneur IoCIContainer
, qui est utilisé pour cela, et laComponent
classe a uneGetService
méthode qui est le localisateur de service réel. Bien sûr, rien ne vous empêche d'utiliser tout ou partie de ces interfaces sans leIContainer
ou ce localisateur particulier. Les services eux-mêmes ne sont que faiblement couplés au conteneur.Les contrats dans WCF sont entièrement construits autour d'interfaces. La classe de service concrète réelle est généralement référencée par son nom dans un fichier de configuration, qui est essentiellement DI. Beaucoup de gens ne s'en rendent pas compte, mais il est tout à fait possible d'échanger ce système de configuration avec un autre conteneur IoC. Peut-être plus intéressant encore, les comportements de service sont tous des exemples
IServiceBehavior
qui peuvent être ajoutés ultérieurement. Encore une fois, vous pouvez facilement câbler cela dans un conteneur IoC et lui faire choisir les comportements pertinents, mais la fonctionnalité est complètement utilisable sans un.Et ainsi de suite. Vous trouverez la DI partout dans .NET, c'est juste que normalement cela se fait de manière si transparente que vous ne la considérez même pas comme DI.
Si vous souhaitez concevoir votre bibliothèque compatible DI pour une utilisation maximale, la meilleure suggestion est probablement de fournir votre propre implémentation IoC par défaut à l'aide d'un conteneur léger.
IContainer
est un excellent choix pour cela car il fait partie du .NET Framework lui-même.la source
IContainer
n'est pas réellement le conteneur dans un paragraphe. ;)private readonly
champs? C'est génial s'ils ne sont utilisés que par la classe déclarante, mais l'OP a spécifié qu'il s'agissait d'un code de niveau framework / bibliothèque, ce qui implique un sous-classement. Dans de tels cas, vous souhaitez généralement que les dépendances importantes soient exposées aux sous-classes. J'aurais pu écrire unprivate readonly
champ et une propriété avec des getters / setters explicites, mais ... gaspillé de l'espace dans un exemple, et plus de code à maintenir en pratique, sans réel avantage.EDIT 2015 : le temps a passé, je me rends compte maintenant que tout cela était une énorme erreur. Les conteneurs IoC sont terribles et DI est un très mauvais moyen de gérer les effets secondaires. En effet, toutes les réponses ici (et la question elle-même) doivent être évitées. Soyez simplement conscient des effets secondaires, séparez-les du code pur, et tout le reste se met en place ou est d'une complexité inutile et inutile.
La réponse originale suit:
J'ai dû faire face à cette même décision lors du développement de SolrNet . J'ai commencé avec l'objectif d'être compatible avec les DI et agnostique aux conteneurs, mais en ajoutant de plus en plus de composants internes, les usines internes sont rapidement devenues ingérables et la bibliothèque résultante était inflexible.
J'ai fini par écrire mon propre conteneur IoC intégré très simple tout en fournissant une installation de Windsor et un module Ninject . L'intégration de la bibliothèque avec d'autres conteneurs est juste une question de câblage correct des composants, donc je pourrais facilement l'intégrer à Autofac, Unity, StructureMap, peu importe.
L'inconvénient est que j'ai perdu la possibilité de simplement
new
augmenter le service. J'ai également pris une dépendance sur CommonServiceLocator que j'aurais pu éviter (je pourrais le refactoriser à l'avenir) pour rendre le conteneur intégré plus facile à implémenter.Plus de détails dans cet article de blog .
MassTransit semble s'appuyer sur quelque chose de similaire. Il a une interface IObjectBuilder qui est vraiment IServiceLocator de CommonServiceLocator avec quelques autres méthodes, puis il l'implémente pour chaque conteneur, c'est-à-dire NinjectObjectBuilder et un module / installation standard , c'est-à-dire MassTransitModule . Il s'appuie ensuite sur IObjectBuilder pour instancier ce dont il a besoin. C'est une approche valable bien sûr, mais personnellement, je ne l'aime pas beaucoup car elle fait trop circuler le conteneur, l'utilisant comme localisateur de service.
MonoRail implémente également son propre conteneur , qui implémente le bon vieux IServiceProvider . Ce conteneur est utilisé dans l'ensemble de ce cadre via une interface qui expose des services bien connus . Pour obtenir le conteneur en béton, il dispose d'un localisateur de fournisseur de services intégré . L' installation de Windsor pointe ce localisateur de fournisseur de services vers Windsor, ce qui en fait le fournisseur de services sélectionné.
Conclusion: il n'y a pas de solution parfaite. Comme pour toute décision de conception, ce problème exige un équilibre entre flexibilité, maintenabilité et commodité.
la source
ServiceLocator
. Si vous appliquez des techniques fonctionnelles, vous vous retrouvez avec l'équivalent de fermetures, qui sont des classes à dépendance (où les dépendances sont les variables fermées). Sinon comment? (Je suis vraiment curieux, parce que je n'aime pas non plus DI.)Ce que je ferais, c'est concevoir ma bibliothèque de manière agnostique pour les conteneurs DI afin de limiter autant que possible la dépendance vis-à-vis du conteneur. Cela permet de remplacer le conteneur DI par un autre si nécessaire.
Ensuite, exposez la couche au-dessus de la logique DI aux utilisateurs de la bibliothèque afin qu'ils puissent utiliser le cadre que vous avez choisi via votre interface. De cette façon, ils peuvent toujours utiliser la fonctionnalité DI que vous avez exposée et ils sont libres d'utiliser tout autre framework à leurs propres fins.
Permettre aux utilisateurs de la bibliothèque de brancher leur propre framework DI me semble un peu faux car cela augmente considérablement la maintenance. Cela devient alors plus un environnement de plugin qu'une DI directe.
la source