web-dev-qa-db-fra.com

Bibliothèque "conviviale" de dépendance (DI)

Je réfléchis à la conception d'une bibliothèque C #, qui aura plusieurs fonctions de haut niveau différentes. Bien entendu, ces fonctions de haut niveau seront implémentées en utilisant autant que possible les principes de conception de la classe . En tant que tels, il y aura probablement des classes destinées à être utilisées directement par les consommateurs, ainsi que des "classes de support" qui sont des dépendances de ces classes plus courantes "d'utilisateurs finaux".

La question est de savoir quel est le meilleur moyen de concevoir la bibliothèque de la manière suivante:

  • DI Agnostic - Bien que l’ajout d’un "support" de base à une ou deux des bibliothèques de DI courantes (StructureMap, Ninject, etc.) semble raisonnable, je souhaite que les consommateurs puissent utiliser la bibliothèque avec n’importe quel cadre de DI.
  • Non utilisable - Si un utilisateur de la bibliothèque n'utilise pas d'identifiant, la bibliothèque doit toujours être aussi facile à utiliser que possible, ce qui réduit le travail que l'utilisateur doit effectuer pour créer toutes ces dépendances "sans importance" juste pour accéder à les "vraies" classes qu'ils veulent utiliser.

Mon idé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 les classes Factory qui ne sont pas DI et contiennent le couplage à ces quelques usines.

Pensées?

225
Pete

C'est en fait simple à faire une fois que vous comprenez que l'ID concerne des modèles et des principes , pas de technologie.

Pour concevoir l'API d'une manière agnostique par conteneur DI, suivez ces principes généraux:

Programmer une interface, pas une implémentation

Ce principe est en réalité une citation (bien que de mémoire) de Design Patterns , mais il devrait toujours être votre but réel . DI n’est qu’un moyen d’atteindre cet objectif.

Appliquer le principe de Hollywood

Le principe d'Hollywood en termes de DI dit: N'appelle pas le conteneur de DI, il t'appellera.

Ne demandez jamais directement une dépendance en appelant un conteneur à partir de votre code. Demandez-le implicitement en utilisant Injection de constructeur .

Utiliser l'injection de constructeur

Lorsque vous avez besoin d'une dépendance, demandez-la statiquement 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 une instance créée, la dépendance est garantie en raison de la combinaison de la clause de garde et du mot clé readonly.

Utilisez Abstract Factory si vous avez besoin d'un objet éphémère

Les dépendances injectées avec l'injection de constructeur ont tendance à être de longue durée, mais vous avez parfois besoin d'un objet de courte durée, ou pour construire la dépendance sur la base d'une valeur connue uniquement au moment de l'exécution.

Voir this pour plus d'informations.

Composer uniquement au dernier moment responsable

Gardez les objets découplés jusqu'à la fin. Normalement, vous pouvez attendre et tout connecter au point d'entrée de l'application. Cela s'appelle la composition racine .

Plus de détails ici:

Simplifiez l'utilisation d'une façade

Si vous estimez que l'API résultante devient trop complexe pour les utilisateurs novices, vous pouvez toujours fournir quelques classes Façade qui encapsulent les combinaisons de dépendances les plus courantes.

Pour fournir une façade flexible avec un degré de découvrabilité élevé, vous pouvez envisager de fournir des constructeurs Fluent. 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();

Cependant, il serait très facile de découvrir qu’il est possible de fournir une dépendance personnalisée et vous pouvez écrire

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

Si vous imaginez que la classe MyFacade encapsule de nombreuses dépendances, j'espère que la manière dont 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 présentés ici et écrit un article plus long sur le blog à propos de Bibliothèques conviviales pour les ID , et un article complémentaire sur Frameworks à l’amélioration des connaissances .

356
Mark Seemann

Le terme "injection de dépendance" n'a aucun rapport spécifique 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 écrivez comme ça:

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; }
}

C'est-à-dire que vous faites deux choses lorsque vous écrivez votre code:

  1. Faites confiance à des interfaces plutôt qu'à des classes chaque fois que vous pensez que l'implémentation pourrait devoir être modifiée.

  2. Au lieu de créer des instances de ces interfaces dans une classe, transmettez-les en tant qu'arguments de constructeur (sinon, elles pourraient être affectées à des propriétés publiques; le premier est injection de constructeur, le dernier est injection de propriété).

Rien de tout cela ne présuppose l'existence d'une bibliothèque DI, et cela ne rend pas le code plus difficile à écrire sans.

Si vous cherchez un exemple de cela, ne cherchez pas plus loin que le .NET Framework lui-même:

  • List<T> met en oeuvre IList<T>. Si vous concevez votre classe pour utiliser IList<T> (ou IEnumerable<T>), vous pouvez tirer parti de concepts tels que le chargement différé, tels que Linq to SQL, Linq to Entities et NHibernate, qu’ils effectuent en coulisse, généralement par injection de propriétés. Certaines classes de framework acceptent en fait un IList<T> comme argument constructeur, tel que BindingList<T>, utilisé pour plusieurs fonctionnalités de liaison de données.

  • Linq to SQL et EF sont entièrement construits autour de IDbConnection et des interfaces associées, qui peuvent être transmises via les constructeurs publics. Vous n'avez pas besoin de les utiliser, cependant; les constructeurs par défaut fonctionnent parfaitement avec une chaîne de connexion située quelque part dans un fichier de configuration.

  • Si vous travaillez sur des composants WinForms, vous utilisez des "services", tels que INameCreationService ou IExtenderProviderService. Vous ne savez même pas vraiment ce que sont les classes concrètes sont. .NET a en fait son propre conteneur IoC, IContainer, qui s’utilise pour cela, et la classe Component a une méthode GetService qui est le localisateur de service réel. Bien sûr, rien ne vous empêche d’utiliser l’une ou l’ensemble de ces interfaces sans le localisateur IContainer ou 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 toutes des instances de IServiceBehavior qui peuvent être ajoutées ultérieurement. Encore une fois, vous pouvez facilement câbler cela dans un conteneur IoC et le laisser choisir les comportements pertinents, mais la fonctionnalité est complètement utilisable sans aucun.

Et ainsi de suite. Vous trouverez la DID un peu partout dans .NET, c’est juste que cela se fait normalement de façon si transparente que vous ne la considérez même pas comme une DI.

Si vous souhaitez concevoir votre bibliothèque activée par DI pour une utilisation optimale, 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.

38
Aaronaught

EDITER 2015 : le temps a passé, je réalise maintenant que tout cela était une énorme erreur. Les conteneurs IoC sont terribles et l'ID est un très mauvais moyen de faire face aux effets secondaires. Effectivement, 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 tombe en place ou est une complexité inutile et inutile.

La réponse originale suit:


Je devais faire face à cette même décision en développant SolrNet . J'ai commencé avec l'objectif d'être compatible avec les DI et de ne pas prendre en compte les conteneurs, mais au fur et à mesure de l'ajout de composants internes, les usines internes sont rapidement devenues ingérables et la bibliothèque résultante inflexible.

J'ai fini par écrire mon propre conteneur IoC intégré très simple tout en fournissant également un installation de Windsor et un module Ninject . L’intégration de la bibliothèque à d’autres conteneurs n’est qu’une question de câblage adéquat des composants, ce qui me permet de l’intégrer facilement à Autofac, Unity, StructureMap, peu importe.

L'inconvénient est que j'ai perdu la possibilité de simplement new mettre en place le service. J'ai également pris une dépendance sur CommonServiceLocator que j'aurais pu éviter (je pourrais le refactoriser ultérieurement) pour rendre le conteneur incorporé plus facile à implémenter.

Plus de détails dans ce article de blog .

MassTransit semble s'appuyer sur quelque chose de similaire. Il a une interface IObjectBuilder qui est vraiment le IServiceLocator de CommonServiceLocator avec quelques méthodes supplémentaires, puis il l'implémente pour chaque conteneur, c'est-à-dire NinjectObjectBuilder et un module/installation standard, c'est-à-dire - MassTransitModule . Ensuite, il (repose 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 en fait, le conteneur est trop contourné pour être utilisé comme localisateur de services.

MonoRail implémente son propre conteneur également, qui implémente de bons vieux IServiceProvider . Ce conteneur est utilisé dans tout ce cadre via ne interface qui expose des services bien connus . Pour obtenir le conteneur concret, il a un localisateur de fournisseur de services intégré . Le établissement de Windsor pointe ce localisateur de fournisseur de services vers Windsor, ce qui en fait le fournisseur de services sélectionné.

En bout de ligne: il n'y a pas de solution parfaite. Comme pour toute décision de conception, ce problème nécessite un équilibre entre flexibilité, facilité de maintenance et commodité.

4
Mauricio Scheffer

Ce que je ferais serait de concevoir ma bibliothèque selon un moyen agnostique de conteneur DI afin de limiter autant que possible la dépendance à l'égard du conteneur. Cela permet d’échanger le conteneur DIO pour un autre si besoin est.

Exposez ensuite la couche située 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 pour leurs propres besoins.

Permettre aux utilisateurs de la bibliothèque de brancher leur propre framework DI me semble un peu répréhensible car il augmente considérablement la quantité de maintenance. Cela devient alors davantage un environnement de plug-in que le DI droit.

1
Igor Zevaka