4

Pour les besoins de cette discussion, il existe deux types de paramètres qu'un constructeur d'objet peut prendre: dépendance d'état ou dépendance de service. Fournir une dépendance de service avec un conteneur IOC est facile: DI prend le relais. En revanche, les dépendances d'état ne sont généralement connues que du client. Autrement dit, le demandeur d'objet.Paramètres d'état de gestion de conteneur IOC dans un constructeur non défini par défaut

Il s'avère que le fait qu'un client fournisse les paramètres d'état via un conteneur IOC est assez douloureux. Je vais montrer plusieurs façons de faire cela, qui ont tous de gros problèmes, et demander à la communauté s'il y a une autre option qui me manque. Commençons:

Avant ajouté un conteneur du CIO à mon code de projet, j'ai commencé avec une classe comme ceci:

class Foobar { 
    //parameters are state dependencies, not service dependencies 
    public Foobar(string alpha, int omega){...}; 
    //...other stuff 
} 

Je décide d'ajouter un service d'enregistreur depdendency à la classe Foobar, qui peut-être je Je fournirai par le DI:

class Foobar { 
    public Foobar(string alpha, int omega, ILogger log){...}; 
    //...other stuff 
} 

Mais alors on me dit aussi que j'ai besoin de faire de la classe Foobar elle-même "permutable". C'est-à-dire, je suis requis de service-localiser une instance de Foobar. J'ajoute une nouvelle interface dans le mélange:

class Foobar : IFoobar { 
    public Foobar(string alpha, int omega, ILogger log){...}; 
    //...other stuff 
} 

Quand je fais l'appel de localisation de service, il DI la dépendance de service ILogger pour moi. Malheureusement, il n'en va pas de même pour les dépendances d'état Alpha et Omega. Certains conteneurs offrent une syntaxe pour résoudre ce:

//Unity 2.0 pseudo-ish code: 
myContainer.Resolve<IFoobar>(
    new parameterOverride[] { {"alpha", "one"}, {"omega",2} } 
); 

J'aime la fonctionnalité, mais je n'aime pas que ce soit typées et non évident pour le développeur quels paramètres doivent être transmis (via IntelliSense, etc.). Je regarde donc une autre solution:

//This is a "boiler plate" heavy approach! 
class Foobar : IFoobar { 
    public Foobar (string alpha, int omega){...}; 
    //...stuff 
} 
class FoobarFactory : IFoobarFactory { 
    public IFoobar IFoobarFactory.Create(string alpha, int omega){ 
     return new Foobar(alpha, omega); 
    } 
} 

//fetch it... 
myContainer.Resolve<IFoobarFactory>().Create("one", 2); 

solutions précédentes ne résout le problème de type sécurité et IntelliSense, mais il (1) classe forcée Foobar iront chercher un ILogger par un localisateur de services plutôt que DI et (2) il faut moi pour faire un tas de plaque de chaudière (XXXFactory, IXXXFactory) pour toutes les variétés d'implémentations Foobar que je pourrais utiliser. Si je décide d'adopter une approche de localisateur de service pur, cela pourrait ne pas poser de problème. Mais je ne peux toujours pas supporter toutes les chaudières nécessaires pour faire ce travail.

Donc j'essayer une autre variante de l'appui fourni par le conteneur:

//overall, this is a pseudo-memento pattern. 
class Foobar : IFoobar { 
    public Foobar (FoobarParams parms){ 
     this.alpha = parms.alpha; 
     this.omega = parms.omega; 
    }; 
    //...stuff 
} 

class FoobarParams{ 
    public FoobarParams(string alpha, int omega){...}; 
} 

//fetch an instance: 
FoobarParams parms = new FoobarParams("one",2); 
//Unity 2.0 pseudo-code... 
myContainer.resolve<IFoobar>(new parameterOverride(parms)); 

Avec cette approche, j'ai à mi-chemin récupéré mon IntelliSense. Mais je dois attendre jusqu'à l'exécution pour détecter l'erreur où je pourrais oublier de fournir le paramètre "FoobarParams".

Donc, nous allons essayer une nouvelle approche:

//code named "concrete creator" 
class Foobar : IFoobar { 
    public Foobar(string alpha, int omega, ILogger log){...}; 
    static IFoobar Create(string alpha, int omega){ 
     //unity 2.0 pseudo-ish code. Assume a common 
     //service locator, or singleton holds the container... 
     return Container.Resolve<IFoobar>(
      new parameterOverride[] {{"alpha", alpha},{"omega", omega} } 
     ); 
    } 

//Get my instance: 
Foobar.Create("alpha",2); 

Cela ne me dérange pas vraiment que j'utilise le béton « Foobar » classe pour créer un IFoobar. Il représente une base concept que je ne m'attends pas à changer dans mon code. Je ne me soucie pas non plus du manque de sécurité de type dans le "Create" statique, car il est maintenant encapsulé. Mon intellisense travaille aussi! Toute instance concrète ainsi faite ignorera les paramètres d'état fournis s'ils ne s'appliquent pas (comportement Unity 2.0). Peut-être une implémentation concrète différente "FooFoobar" pourrait avoir une discordance formelle de nom d'arg, mais je suis toujours assez content.

Mais le gros problème de cette approche est qu'elle ne fonctionne efficacement qu'avec Unity 2.0 (un paramètre non concordant dans Structure Map lèvera une exception). Donc c'est bon seulement si je reste avec Unity. Le problème est, je commence à aimer Structure Map beaucoup plus. Alors maintenant, je vais sur une autre option:

class Foobar : IFoobar, IFoobarInit { 
    public Foobar(ILogger log){...}; 

    public IFoobar IFoobarInit.Initialize(string alpha, int omega){ 
     this.alpha = alpha; 
     this.omega = omega; 
     return this; 
    } 
} 

//now create it...  
IFoobar foo = myContainer.resolve<IFoobarInit>().Initialize("one", 2) 

Maintenant, avec ce que j'ai un compromis un peu gentil avec les autres approches: (1) Mes arguments sont de type-safe/IntelliSense courant (2) J'ai choix de récupérer ILogger via DI (voir ci-dessus) ou localisateur de service, (3) il n'est pas nécessaire de séparer une ou plusieurs classes FoobarFactory concrètes (contrairement au code d'exemple verbeux "boiler-plate"), et (4) il confirme raisonnablement le principe «rendre les interfaces faciles à utiliser correctement et difficiles à utiliser de manière incorrecte». Au moins, il n'est sans doute pas pire que les alternatives discutées précédemment.

Un obstacle d'acceptation reste encore: je veux aussi appliquer "design by contract". Chaque échantillon que j'ai présenté était intentionnellement en faveur de l'injection du constructeur (pour les dépendances d'état) parce que je veux conserver le support "invariant" comme le plus couramment pratiqué. A savoir, l'invariant est établi lorsque le constructeur est complet.

Dans l'exemple ci-dessus, l'invariant n'est pas établi lorsque la construction de l'objet est terminée. Tant que je fais du «design by contract» en interne, je pourrais juste dire aux développeurs de ne pas tester l'invariant avant que la méthode Initialize (...) ne soit appelée.

Mais surtout, quand .net 4.0 sortira, je veux utiliser son support "contrat de code" pour la conception par contrat. D'après ce que j'ai lu, ce ne sera pas compatible avec cette dernière approche.

Curses!

Bien sûr, il me vient aussi à l'esprit que toute ma philosophie est éteinte. Peut-être qu'on me dirait que conjurer un Foobar: IFoobar via un localisateur de service implique qu'il s'agit d'un service - et les services n'ont d'autres dépendances de service, ils n'ont pas de dépendances d'état (comme l'Alpha et l'Omega). Je suis ouvert à l'écoute de questions aussi philosophiques, mais j'aimerais aussi savoir quelle référence semi-autorisée à lire me guiderait dans cette voie.

Alors maintenant, je le tourne vers la communauté. Quelle approche devrais-je considérer que je n'ai pas encore? Dois-je vraiment croire que j'ai épuisé mes options?

p.s. Ce genre de problème, along with others, me pousse à croire que toute l'idée du conteneur IOC, au moins en .net, est prématurée. Cela me rappelle les jours où les gens se mettaient sur la tête pour que le langage "C" soit orienté objet (en ajoutant des macros étranges et autres). Ce que nous devrions rechercher, c'est le CLR et le soutien linguistique des conteneurs du CIO. Par exemple, imaginez un nouveau type d'interface appelé "initié". Un initié fonctionne comme une interface, mais nécessite également une signature de constructeur particulière. Le reste je laisse comme un exercice à l'étudiant ...

+0

Je ne suis pas sûr que je suivais exactement ce que vous vouliez faire, mais n'est pas l'invariant réelle de votre classe « (non initialisé) ou (t l'invariant que vous dites ne devrait pas être testé avant l'initialisation) "? Certaines langues d'annotation pour les contrats d'écriture vous permettent même d'insérer ce qu'ils appellent un « champ fantôme » pour le booléen « initialisé », mais vous pouvez toujours utiliser un champ normal si ça ne vous dérange pas le mot de mémoire supplémentaire par exemple. –

Répondre

3

Qu'est-ce que vous cherchez est le "adaptateur d'usine" ou Func < T, U >.

Un composant qui doit créer des instances paramétrées d'un autre composant à la volée utilise le Func < T, le type de dépendance U > pour représenter ceci:

class Bar 
{ 
    Func<string, int, IFoo> _fooCreator; 

    public Bar(Func<string, int, IFoo> fooCreator) { 
     _fooCreator = fooCreator; 
    } 

    public void Go() { 
     var foo = _fooCreator("a", 42); 
     // ... 
    } 
} 

Autofac, par exemple, fournira automatiquement le Func chaque fois IFoo est enregistré. Il va également fusionner les paramètres dans le constructeur Foo (y compris ILogger,) et rejeter ceux qui sont inutiles plutôt que de lancer une erreur.

Autofac prend également en charge les délégués personnalisés comme:

delegate IFoo FooCreator(string alpha, int omega); 

De cette façon, Bar peut être réécrite:

class Bar 
{ 
    FooCreator _fooCreator; 

    public Bar(FooCreator fooCreator) { 
     _fooCreator = fooCreator; 
    } 

    public void Go() { 
     var foo = _fooCreator("a", 42); 
     // ... 
    } 
} 

Lorsque les délégués personnalisés sont utilisés, les paramètres seront jumelés par nom, plutôt que par type comme pour Func.

Autofac a la documentation que vous pouvez consulter: http://code.google.com/p/autofac/wiki/DelegateFactories.

Il y a un projet de collaboration dans les travaux entre plusieurs communautés de conteneurs du CIO appelés « adaptateurs communs de contexte » pour normaliser ces derniers et d'autres types de dépendance d'ordre supérieur. Le site du projet est au http://cca.codeplex.com.

La première spécification proposée de l'ACC couvre l'adaptateur d'usine, vous pouvez le lire ici: http://cca.codeplex.com/wikipage?title=FuncFactoryScenarios&referringTitle=Documentation.

D'autres liens peuvent vous être utiles:

http://nblumhardt.com/2010/04/introducing-autofac-2-1-rtw/ http://nblumhardt.com/2010/01/the-relationship-zoo/

J'espère que vous trouverez que si les conteneurs ont certes un IoC long chemin à parcourir avant qu'ils seront complètement transparents, nous » travaillez en fait assez dur pour y arriver :)

Nick

+0

J'apprécie vraiment cette alternative que vous avez énumérée. Certes, il a trop limites (il devient gênant pour 2 ou plusieurs dépendances de service et ne supporte pas le « invariant » de .net 4.0 « le code des contrats »). Mais vous avez dit que les conteneurs IoC ont un long chemin à parcourir. Les commentaires honnêtes sont ce dont j'ai besoin plus que tout. :) –

0

Si ces paramètres sont constants pour la durée de vie de l'application, alors vous pouvez ajouter un IConfigurationService dont le seul but est de renvoyer ces paramètres à ceux qui en ont besoin. L'implémentation de IConfigurationService peut avoir des valeurs codées en dur, les lire depuis un fichier de configuration ... peu importe. Bien sûr, l'implémentation de IConfigurationService est récupérée via le conteneur IoC.

Si ces paramètres varient d'une instance à l'autre, je ne pense pas qu'ils doivent être fournis en tant que paramètres de constructeur pour les objets chargés par votre conteneur IoC. Cela rend tous vos composants nécessaires pour trouver/dépendre du conteneur IoC, ce qui, en premier lieu, détruit le point d'un conteneur IoC.Soit les rendre configurables sur l'objet lui-même en exposant une méthode setter, (ce qui est approprié quand ils peuvent changer au cours de la vie de l'objet) ou en faire des paramètres à une méthode usine qui renvoie l'objet (où l'objet factory est enregistré dans votre conteneur IoC).

Vous hésitaient à utiliser une usine, mais je pense que c'est une approche élégante. Oui, il faut de l'effort pour créer une usine, mais comme il y a une exigence à l'appui de la loi, ce n'est pas le gonflement du code. C'est un modèle simple qui répond à l'exigence.