Bibliothèque «conviviale» d'Injection de dépendance (DI)

230

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?

Pete
la source
La nouvelle référence à l'article de lien brisé (SOLID): butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
M.Hassan

Réponses:

360

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:

public class Service : IService
{
    private readonly ISomeDependency dep;

    public Service(ISomeDependency dep)
    {
        if (dep == null)
        {
            throw new ArgumentNullException("dep");
        }

        this.dep = dep;
    }

    public ISomeDependency Dependency
    {
        get { return this.dep; }
    }
}

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 readonlymot 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:

public class MyFacade
{
    private IMyDependency dep;

    public MyFacade()
    {
        this.dep = new DefaultDependency();
    }

    public MyFacade WithDependency(IMyDependency dependency)
    {
        this.dep = dependency;
        return this;
    }

    public Foo CreateFoo()
    {
        return new Foo(this.dep);
    }
}

Cela permettrait à un utilisateur de créer un Foo par défaut en écrivant

var foo = new MyFacade().CreateFoo();

Il serait cependant très découvrable qu'il est possible de fournir une dépendance personnalisée, et vous pourriez écrire

var foo = new MyFacade().WithDependency(new CustomDependency()).CreateFoo();

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 .

Mark Seemann
la source
21
Bien que cela sonne bien sur le papier, d'après mon expérience, une fois que vous avez beaucoup de composants internes interagissant de manière complexe, vous vous retrouvez avec beaucoup d'usines à gérer, ce qui rend la maintenance plus difficile. De plus, les usines devront gérer le style de vie de leurs composants créés, une fois que vous aurez installé la bibliothèque dans un vrai conteneur, cela entrera en conflit avec la gestion du style de vie du conteneur. Les usines et les façades gênent les vrais conteneurs.
Mauricio Scheffer
4
Je n'ai pas encore trouvé de projet qui fasse tout cela.
Mauricio Scheffer
31
Eh bien, c'est comme ça que nous développons des logiciels chez Safewhere, donc nous ne partageons pas votre expérience ...
Mark Seemann
19
Je pense que les façades devraient être codées à la main car elles représentent des combinaisons connues (et probablement communes) de composants. Un conteneur DI ne devrait pas être nécessaire, car tout peut être câblé à la main (pensez à la DI du pauvre). Rappelez-vous qu'une façade n'est qu'une classe de commodité facultative pour les utilisateurs de votre API. Les utilisateurs avancés peuvent toujours vouloir contourner la façade et câbler les composants à leur guise. Ils peuvent vouloir utiliser leur propre DI Contaier pour cela, donc je pense qu'il serait méchant de forcer un conteneur DI particulier sur eux s'ils ne vont pas l'utiliser. Possible mais pas conseillé
Mark Seemann
8
C'est peut-être la meilleure réponse que j'ai jamais vue sur SO.
Nick Hodges
40

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:

public class Service
{
    public Service()
    {
    }

    public void DoSomething()
    {
        SqlConnection connection = new SqlConnection("some connection string");
        WindowsIdentity identity = WindowsIdentity.GetCurrent();
        // Do something with connection and identity variables
    }
}

Vous l'écrivez comme ceci:

public class Service
{
    public Service(IDbConnection connection, IIdentity identity)
    {
        this.Connection = connection;
        this.Identity = identity;
    }

    public void DoSomething()
    {
        // Do something with Connection and Identity properties
    }

    protected IDbConnection Connection { get; private set; }
    protected IIdentity Identity { get; private set; }
}

Autrement dit, vous faites deux choses lorsque vous écrivez votre code:

  1. Fiez-vous aux interfaces plutôt qu'aux classes chaque fois que vous pensez que l'implémentation doit être modifiée;

  2. 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 œuvre IList<T>. Si vous concevez votre classe à utiliser IList<T>(ou IEnumerable<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 acceptent IList<T>en fait un argument comme constructeur, tel que BindingList<T>, qui est utilisé pour plusieurs fonctionnalités de liaison de données.

  • Linq to SQL et EF sont entièrement construits autour IDbConnectiondes 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 INameCreationServiceou IExtenderProviderService. Vous ne savez même pas vraiment quelles sont les classes concrètes . .NET a en fait son propre conteneur IoC IContainer, qui est utilisé pour cela, et la Componentclasse a une GetServicemé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 le IContainerou 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 IServiceBehaviorqui 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. IContainerest un excellent choix pour cela car il fait partie du .NET Framework lui-même.

Aaronaught
la source
2
La véritable abstraction pour un conteneur est IServiceProvider, pas IContainer.
Mauricio Scheffer
2
@Mauricio: Vous avez bien sûr raison, mais essayez d'expliquer à quelqu'un qui n'a jamais utilisé le système LWC pourquoi ce IContainern'est pas réellement le conteneur dans un paragraphe. ;)
Aaronaught
Aaron, juste curieux de savoir pourquoi vous utilisez un ensemble privé; au lieu de simplement spécifier les champs en lecture seule?
jr3
@Jreeter: Vous voulez dire pourquoi ne sont-ils pas des private readonlychamps? 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 un private readonlychamp 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.
Aaronaught
Merci d'avoir clarifié! J'ai supposé que les champs en lecture seule seraient accessibles à l'enfant.
jr3
5

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 newaugmenter 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é.

Mauricio Scheffer
la source
12
Bien que je respecte votre opinion, je trouve vos propos trop généralisés "toutes les autres réponses ici sont dépassées et doivent être évitées" extrêmement naïves. Si nous avons appris quelque chose depuis, c'est que l'injection de dépendance est une réponse aux limitations du langage. Sans faire évoluer ou abandonner nos langages actuels, il semble que nous aurons besoin de DI pour aplatir nos architectures et atteindre la modularité. L'IoC en tant que concept général n'est qu'une conséquence de ces objectifs et certainement souhaitable. Les conteneurs IoC ne sont absolument pas nécessaires pour IoC ou DI, et leur utilité dépend des compositions de modules que nous fabriquons.
TNE
@tne Votre mention de moi étant "extrêmement naïf" est une ad-hominem non bienvenue. J'ai écrit des applications complexes sans DI ou IoC en C #, VB.NET, Java (langages que vous penseriez "avoir besoin" d'IoC à en juger par votre description), il ne s'agit certainement pas de limitations de langages. Il s'agit de limitations conceptuelles. Je vous invite à en apprendre davantage sur la programmation fonctionnelle au lieu de recourir à des attaques ad hominem et à de mauvais arguments.
Mauricio Scheffer
18
J'ai mentionné que je trouvais votre déclaration naïve; cela ne dit rien de vous personnellement et est tout à mon avis. Veuillez ne pas vous sentir insulté par un inconnu au hasard. Impliquer que j'ignore toutes les approches de programmation fonctionnelle pertinentes n'est pas non plus utile; vous ne le savez pas et vous vous trompez. Je savais que vous y connaissiez bien (j'ai rapidement regardé votre blog), c'est pourquoi j'ai trouvé ennuyeux qu'un gars apparemment bien informé dise des choses comme "nous n'avons pas besoin d'IoC". IoC arrive littéralement partout dans la programmation fonctionnelle (..)
TNE
4
et dans les logiciels de haut niveau en général. En fait, les langages fonctionnels (et de nombreux autres styles) prouvent en fait qu'ils résolvent l'IoC sans DI, je pense que nous sommes tous les deux d'accord avec cela, et c'est tout ce que je voulais dire. Si vous avez réussi sans DI dans les langues que vous mentionnez, comment avez-vous procédé? Je suppose que non en utilisant 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.)
Le
1
Les conteneurs IoC sont des dictionnaires mutables globaux, supprimant la paramétricité comme un outil de raisonnement, donc à éviter. L'IoC (le concept) comme dans «ne nous appelez pas, nous vous appellerons» s'applique techniquement chaque fois que vous passez une fonction comme valeur. Au lieu de DI, modélisez votre domaine avec des ADT puis écrivez des interprètes (cf Free).
Mauricio Scheffer
1

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.

Igor Zevaka
la source