web-dev-qa-db-fra.com

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

J'essaie de faire en sorte qu'Unity gère la création de mes objets et je souhaite disposer de paramètres d'initialisation inconnus jusqu'au moment de l'exécution:

Pour le moment, la seule façon pour moi 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, runTimeParam param est déterminé au moment de l'exécution en fonction des entrées de l'utilisateur. Le cas trivial ici renvoie simplement la valeur de runTimeParam, mais en réalité, le paramètre sera quelque chose comme le nom du fichier et la méthode initialize fera quelque chose avec le fichier.

Cela crée un certain nombre de problèmes, notamment le fait que la méthode Initialize est disponible sur l'interface et peut être appelée plusieurs fois. La définition d'un indicateur dans l'implémentation et le lancement d'une exception lors d'un appel répété sur Initialize semblent plutôt maladroits.

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

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

145
Igor Zevaka

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 Leaky Abstraction.

Dans votre cas, je dirais que vous devriez modéliser l'interface IMyIntf sur de la manière dont vous devez l'utiliser et non sur la façon dont vous souhaitez créer. leurs mises en œuvre. C'est un détail de mise en œuvre.

Ainsi, l'interface devrait simplement être:

public interface IMyIntf
{
    string RunTimeParam { get; }
}

Définissez maintenant l’abstrait Factory:

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

Vous pouvez maintenant créer une implémentation concrète de IMyIntfFactory qui crée des instances concrètes de IMyIntf comme 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 mot clé readonly. Aucune odeur Initiez des méthodes sont nécessaires.

Une implémentation de IMyIntfFactory peut être aussi simple que ceci:

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

Dans tous vos consommateurs où vous avez besoin d'une instance IMyIntf, vous prenez simplement une dépendance sur IMyIntfFactory en la demandant par le biais de Constructor Injection.

Tout conteneur DI digne de ce nom sera en mesure de connecter automatiquement une instance IMyIntfFactory si vous l’enregistrez correctement.

268
Mark Seemann

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

Si vous avez besoin de passer un paramètre spécifique au contexte dans le constructeur, vous pouvez, par exemple, créer une fabrique qui résout vos dépendances de service via le constructeur et prend votre paramètre d'exécution en tant que paramètre de la méthode Create () (ou Generate ( ), Build () ou 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 Qu'est-ce qui empêche quelqu'un de rappeler l'initialisation ou le setter?).

14
Phil Sandler

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 message Stackoverflow ).

J'ai aimé comment Ninject extension qui vous permet de créer dynamiquement des fabriques 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 sur le IUnityContainer , qui vous permet d'enregistrer des fabriques qui créeront de nouveaux objets à partir de données provenant d'objets existants, essentiellement en correspondance avec une hiérarchie de types. vers une hiérarchie de types différente: nityMappingFactory @ GitHub

Avec un objectif 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 ou des interfaces de fabrique individuelles (un gain de temps réel). Vous ajoutez simplement les mappages à l'endroit où vous enregistrez les classes pendant le processus d'amorçage 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 la fabrique de mappages dans le constructeur de 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 d'objet.

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

5
jigamiller

Je pense que je l'ai résolu et que ça semble plutôt sain, alors ça doit être à moitié juste :))

Je divise IMyIntf en interfaces "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 de Service Locator , nous pouvons le résumer assez bien pour que IMyIntfSetter soit presque une interface interne distincte. de IMyIntf.

1
Igor Zevaka

Je ne peux pas vous répondre avec une terminologie spécifique à Unity, mais il semblerait que vous en appreniez seulement sur l'injection de dépendance. Si tel est le cas, je vous prie de lire attentivement le résumé, les informations claires et les informations très diverses guide de l'utilisateur pour Ninject .

Cela vous guidera à travers les différentes options que vous avez lorsque vous utilisez DI, et vous expliquera comment vous rendre compte des problèmes spécifiques que vous rencontrerez 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 reçoive une référence à chacune de ses dépendances via le constructeur.

La procédure pas à pas explique é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 expliquera les concepts et la terminologie de la fonctionnalité qui convient à votre objectif. Vous devriez pouvoir mapper ces connaissances sur Unity ou d'autres cadres DI (ou convaincre de tenter votre chance avec Ninject). .

1
anthony