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 ...
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. –