Existe-t-il un modèle d'initialisation des objets créés via un conteneur DI

147

J'essaie de faire en sorte que Unity gère la création de mes objets et je souhaite avoir des paramètres d'initialisation qui ne sont pas connus avant l'exécution:

Pour le moment, la seule façon dont je pourrais penser à la façon de le faire est d'avoir une méthode Init sur l'interface.

interface IMyIntf {
  void Initialize(string runTimeParam);
  string RunTimeParam { get; }
}

Ensuite, pour l'utiliser (dans Unity), je ferais ceci:

var IMyIntf = unityContainer.Resolve<IMyIntf>();
IMyIntf.Initialize("somevalue");

Dans ce scénario, le runTimeParamparamètre est déterminé au moment de l'exécution en fonction de l'entrée utilisateur. Le cas trivial ici renvoie simplement la valeur de runTimeParammais en réalité le paramètre sera quelque chose comme le nom de fichier et la méthode d'initialisation fera quelque chose avec le fichier.

Cela crée un certain nombre de problèmes, à savoir que la Initializeméthode est disponible sur l'interface et peut être appelée plusieurs fois. Définir un indicateur dans l'implémentation et lancer une exception lors d'un appel répété à Initializesemble trop maladroit.

Au moment où je résous mon interface, je ne veux rien savoir de l'implémentation de IMyIntf. Ce que je veux, cependant, c'est savoir que cette interface a besoin de certains paramètres d'initialisation ponctuels. Existe-t-il un moyen d'annoter (attributs?) L'interface avec ces informations et de les transmettre au framework lorsque l'objet est créé?

Edit: décrit un peu plus l'interface.

Igor Zevaka
la source
9
Vous manquez le point d'utiliser un conteneur DI. Les dépendances sont censées être résolues pour vous.
Pierreten
D'où obtenez-vous vos paramètres nécessaires? (fichier de configuration, db, ??)
Jaime
runTimeParamest une dépendance déterminée lors de l'exécution en fonction d'une entrée utilisateur. L'alternative devrait-elle être de la diviser en deux interfaces - une pour l'initialisation et une autre pour stocker les valeurs?
Igor Zevaka
dépendance dans IoC, fait généralement référence à la dépendance à d'autres classes ou objets de type ref qui peuvent être déterminés lors de la phase d'initialisation IoC. Si votre classe n'a besoin que de quelques valeurs pour fonctionner, c'est là que la méthode Initialize () de votre classe devient pratique.
La lumière
Je veux dire, imaginez qu'il y a 100 classes dans votre application sur lesquelles cette approche peut être appliquée; alors vous devrez créer 100 classes d'usine supplémentaires + 100 interfaces pour vos classes et vous pourriez vous en tirer si vous utilisiez simplement la méthode Initialize ().
The Light

Réponses:

276

N'importe quel endroit où vous avez besoin d'une valeur d'exécution pour construire une dépendance particulière, Abstract Factory est la solution.

Avoir des méthodes Initialize sur les interfaces sent une abstraction baveuse .

Dans votre cas, je dirais que vous devez modéliser l' IMyIntfinterface sur la façon dont vous devez l'utiliser - et non sur la façon dont vous avez l'intention d'en créer des implémentations. C'est un détail de mise en œuvre.

Ainsi, l'interface devrait simplement être:

public interface IMyIntf
{
    string RunTimeParam { get; }
}

Définissez maintenant la fabrique abstraite:

public interface IMyIntfFactory
{
    IMyIntf Create(string runTimeParam);
}

Vous pouvez maintenant créer une implémentation concrète de IMyIntfFactoryqui crée des instances concrètes IMyIntfcomme celle-ci:

public class MyIntf : IMyIntf
{
    private readonly string runTimeParam;

    public MyIntf(string runTimeParam)
    {
        if(runTimeParam == null)
        {
            throw new ArgumentNullException("runTimeParam");
        }

        this.runTimeParam = runTimeParam;
    }

    public string RunTimeParam
    {
        get { return this.runTimeParam; }
    }
}

Remarquez comment cela nous permet de protéger les invariants de la classe en utilisant le readonlymot - clé. Aucune méthode d'initialisation malodorante n'est nécessaire.

Une IMyIntfFactoryimplémentation peut être aussi simple que ceci:

public class MyIntfFactory : IMyIntfFactory
{
    public IMyIntf Create(string runTimeParam)
    {
        return new MyIntf(runTimeParam);
    }
}

Dans tous vos consommateurs pour lesquels vous avez besoin d'une IMyIntfinstance, il vous suffit de prendre une dépendance IMyIntfFactoryen la demandant via l' injection de constructeur .

Tout conteneur DI digne de ce nom sera en mesure de câbler automatiquement une IMyIntfFactoryinstance pour vous si vous l'enregistrez correctement.

Mark Seemann
la source
13
Le problème est qu'une méthode (comme Initialize) fait partie de votre API, alors que le constructeur ne l'est pas. blog.ploeh.dk/2011/02/28/InterfacesAreAccessModifiers.aspx
Mark Seemann
13
De plus, une méthode Initialize indique le couplage temporel: blog.ploeh.dk/2011/05/24/DesignSmellTemporalCoupling.aspx
Mark Seemann
2
@Darlene Vous pourrez peut-être utiliser un Decorator paresseusement initialisé, comme décrit dans la section 8.3.6 de mon livre . Je donne également un exemple de quelque chose de similaire dans ma présentation Big Object Graphs Up Front .
Mark Seemann
2
@Mark Si la création de l' MyIntfimplémentation par l'usine nécessite plus de runTimeParam(lire: d'autres services que l'on voudrait résoudre par un IoC), vous êtes toujours confronté à la résolution de ces dépendances dans votre usine. J'aime la réponse @PhilSandler consistant à transmettre ces dépendances au constructeur de l' usine pour résoudre ce problème - est-ce que c'est également votre opinion ?
Jeff
2
Aussi bien, mais votre réponse à cette autre question est vraiment venue à mon avis.
Jeff
15

Habituellement, lorsque vous rencontrez cette situation, vous devez revoir votre conception et déterminer si vous mélangez vos objets avec état / données avec vos services purs. Dans la plupart des cas (pas tous), vous souhaiterez conserver ces deux types d'objets séparés.

Si vous avez besoin d'un paramètre spécifique au contexte passé dans le constructeur, une option consiste à créer une fabrique qui résout vos dépendances de service via le constructeur et prend votre paramètre d'exécution comme paramètre de la méthode Create () (ou Generate ( ), Build () ou tout ce que vous nommez vos méthodes d'usine).

Avoir des setters ou une méthode Initialize () est généralement considéré comme une mauvaise conception, car vous devez "vous rappeler" de les appeler et vous assurer qu'ils n'ouvrent pas trop l'état de votre implémentation (c'est-à-dire ce qui empêche quelqu'un de re -appel initialize ou le setter?).

Phil Sandler
la source
5

J'ai également rencontré cette situation à quelques reprises dans des environnements où je crée dynamiquement des objets ViewModel basés sur des objets Model (très bien décrits par cet autre article de Stackoverflow ).

J'ai aimé l' extension Ninject qui permet de créer dynamiquement des usines basées sur des interfaces:

Bind<IMyFactory>().ToFactory();

Je n'ai trouvé aucune fonctionnalité similaire directement dans Unity ; j'ai donc écrit ma propre extension pour IUnityContainer qui vous permet d'enregistrer des usines qui créeront de nouveaux objets basés sur les données d'objets existants mappant essentiellement d'une hiérarchie de types à une hiérarchie de types différente: UnityMappingFactory @ GitHub

Dans un but de simplicité et de lisibilité, je me suis retrouvé avec une extension qui vous permet de spécifier directement les mappages sans déclarer des classes d'usine ou des interfaces individuelles (un gain de temps réel). Vous ajoutez simplement les mappages là où vous enregistrez les classes pendant le processus de démarrage normal ...

//make sure to register the output...
container.RegisterType<IImageWidgetViewModel, ImageWidgetViewModel>();
container.RegisterType<ITextWidgetViewModel, TextWidgetViewModel>();

//define the mapping between different class hierarchies...
container.RegisterFactory<IWidget, IWidgetViewModel>()
.AddMap<IImageWidget, IImageWidgetViewModel>()
.AddMap<ITextWidget, ITextWidgetViewModel>();

Ensuite, vous déclarez simplement l'interface de fabrique de mappage dans le constructeur pour CI et utilisez sa méthode Create () ...

public ImageWidgetViewModel(IImageWidget widget, IAnotherDependency d) { }

public TextWidgetViewModel(ITextWidget widget) { }

public ContainerViewModel(object data, IFactory<IWidget, IWidgetViewModel> factory)
{
    IList<IWidgetViewModel> children = new List<IWidgetViewModel>();
    foreach (IWidget w in data.Widgets)
        children.Add(factory.Create(w));
}

En prime, toutes les dépendances supplémentaires dans le constructeur des classes mappées seront également résolues lors de la création de l'objet.

Évidemment, cela ne résoudra pas tous les problèmes, mais cela m'a plutôt bien servi jusqu'à présent, alors j'ai pensé que je devrais le partager. Il y a plus de documentation sur le site du projet sur GitHub.

jigamiller
la source
1

Je ne peux pas répondre avec une terminologie spécifique à Unity, mais il semble que vous soyez en train d'apprendre l'injection de dépendances. Si tel est le cas, je vous exhorte à lire le guide de l'utilisateur bref, clair et riche en informations de Ninject .

Cela vous guidera à travers les différentes options que vous avez lors de l'utilisation de DI et comment prendre en compte les problèmes spécifiques auxquels vous serez confronté en cours de route. Dans votre cas, vous souhaiterez probablement utiliser le conteneur DI pour instancier vos objets, et faire en sorte que cet objet obtienne une référence à chacune de ses dépendances via le constructeur.

La procédure pas à pas détaille également comment annoter des méthodes, des propriétés et même des paramètres à l'aide d'attributs pour les distinguer au moment de l'exécution.

Même si vous n'utilisez pas Ninject, la procédure pas à pas vous donnera les concepts et la terminologie de la fonctionnalité qui convient à votre objectif, et vous devriez être en mesure de mapper ces connaissances à Unity ou à d'autres frameworks DI (ou vous convaincre d'essayer Ninject) .

Anthony
la source
Merci pour ça. J'évalue en fait des frameworks DI et NInject allait être mon prochain.
Igor Zevaka
@johann: fournisseurs? github.com/ninject/ninject/wiki/…
anthony
1

Je pense que je l'ai résolu et que c'est plutôt sain, donc ça doit être à moitié correct :))

Je me suis divisé IMyIntfen une interface "getter" et "setter". Alors:

interface IMyIntf {
  string RunTimeParam { get; }
}


interface IMyIntfSetter {
  void Initialize(string runTimeParam);
  IMyIntf MyIntf {get; }
}

Puis la mise en œuvre:

class MyIntfImpl : IMyIntf, IMyIntfSetter {
  string _runTimeParam;

  void Initialize(string runTimeParam) {
    _runTimeParam = runTimeParam;
  }

  string RunTimeParam { get; }

  IMyIntf MyIntf {get {return this;} }
}

//Unity configuration:
//Only the setter is mapped to the implementation.
container.RegisterType<IMyIntfSetter, MyIntfImpl>();
//To retrieve an instance of IMyIntf:
//1. create the setter
IMyIntfSetter setter = container.Resolve<IMyIntfSetter>();
//2. Init it
setter.Initialize("someparam");
//3. Use the IMyIntf accessor
IMyIntf intf = setter.MyIntf;

IMyIntfSetter.Initialize() peut toujours être appelé plusieurs fois mais en utilisant des bits de paradigme du localisateur service, nous pouvons le résumer assez bien pour qu'il s'agisse IMyIntfSetterpresque d'une interface interne distincte de IMyIntf.

Igor Zevaka
la source
13
Ce n'est pas une solution particulièrement bonne car elle repose sur une méthode Initialize, qui est une abstraction fuyante. Btw, cela ne ressemble pas à Service Locator, mais plutôt à l'Injection d'Interface. Dans tous les cas, consultez ma réponse pour une meilleure solution.
Mark Seemann