it-swarm.com.de

Gibt es ein Muster zum Initialisieren von Objekten, die über einen DI-Container erstellt wurden?

Ich versuche, Unity dazu zu bringen, die Erstellung meiner Objekte zu verwalten, und möchte einige Initialisierungsparameter haben, die erst zur Laufzeit bekannt sind:

Momentan kann ich mir nur vorstellen, wie das geht, indem ich eine Init-Methode auf der Schnittstelle habe.

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

Um es dann (in Unity) zu benutzen, würde ich folgendes tun:

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

In diesem Szenario wird runTimeParam param zur Laufzeit basierend auf Benutzereingaben bestimmt. Der triviale Fall hier gibt einfach den Wert von runTimeParam zurück, aber in Wirklichkeit ist der Parameter so etwas wie Dateiname und die Initialisierungsmethode tut etwas mit der Datei.

Dies führt zu einer Reihe von Problemen: Die Methode Initialize ist auf der Schnittstelle verfügbar und kann mehrmals aufgerufen werden. Das Setzen eines Flags in der Implementierung und das Auslösen einer Ausnahme bei wiederholtem Aufruf von Initialize scheint sehr umständlich.

An dem Punkt, an dem ich meine Schnittstelle aufgelöst habe, möchte ich nichts über die Implementierung von IMyIntf wissen. Was ich jedoch möchte, ist das Wissen, dass diese Schnittstelle bestimmte einmalige Initialisierungsparameter benötigt. Gibt es eine Möglichkeit, die Schnittstelle mit diesen Informationen zu versehen und diese beim Erstellen des Objekts an das Framework zu übergeben?

Edit: Beschrieb die Oberfläche etwas mehr.

145
Igor Zevaka

Jeder Ort, an dem Sie einen Laufzeitwert benötigen, um eine bestimmte Abhängigkeit zu erstellen, Abstract Factory, ist die Lösung.

Wenn Initialize-Methoden auf den Interfaces vorhanden sind, riecht es nach Leaky Abstraction.

In Ihrem Fall würde ich sagen, dass Sie die IMyIntf -Schnittstelle nachbilden sollten wie Sie sie verwenden müssen - nicht wie Sie beabsichtigen, Implementierungen davon zu erstellen. Das ist ein Implementierungsdetail.

Die Schnittstelle sollte also einfach sein:

public interface IMyIntf
{
    string RunTimeParam { get; }
}

Definieren Sie nun die Abstract Factory:

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

Sie können jetzt eine konkrete Implementierung von IMyIntfFactory erstellen, die konkrete Instanzen von IMyIntf wie diese erstellt:

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

Beachten Sie, wie dies uns ermöglicht, die Invarianten der Klasse zu schützen mit dem Schlüsselwort readonly. Es sind keine stinkenden Initialisierungsmethoden erforderlich.

Eine IMyIntfFactory -Implementierung kann so einfach sein:

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

In all Ihren Konsumenten, in denen Sie eine IMyIntf -Instanz benötigen, nehmen Sie einfach eine Abhängigkeit von IMyIntfFactory, indem Sie diese über Constructor Injection anfordern.

Jeder DI-Container, der sein Salz wert ist, kann eine IMyIntfFactory -Instanz automatisch verbinden, wenn Sie ihn korrekt registrieren.

268
Mark Seemann

In der Regel müssen Sie in dieser Situation Ihr Design überprüfen und feststellen, ob Sie Ihre Stateful-/Datenobjekte mit Ihren reinen Diensten mischen. In den meisten (nicht allen) Fällen möchten Sie diese beiden Arten von Objekten getrennt halten.

Wenn Sie einen im Konstruktor übergebenen kontextspezifischen Parameter benötigen, können Sie eine Factory erstellen, die Ihre Dienstabhängigkeiten über den Konstruktor auflöst, und Ihren Laufzeitparameter als Parameter der Create () - Methode (oder Generate () - Methode verwenden. ), Build () oder wie auch immer Sie Ihre Factory-Methoden nennen).

Setter oder eine Initialize () -Methode gelten im Allgemeinen als schlechtes Design, da Sie sich "erinnern" müssen, um sie aufzurufen, und sicherstellen müssen, dass sie nicht zu viel vom Status Ihrer Implementierung öffnen (d. H was soll jemanden davon abhalten, initialize oder den setter erneut aufzurufen?).

14
Phil Sandler

Ich habe diese Situation auch ein paar Mal in Umgebungen erlebt, in denen ich ViewModel-Objekte dynamisch auf der Basis von Modellobjekten erstelle (dies wird in diesem anderen Stackoverflow-Post sehr gut umrissen).

Mir hat gefallen, wie die Ninject-Erweiterung ermöglicht, Fabriken basierend auf Schnittstellen dynamisch zu erstellen:

Bind<IMyFactory>().ToFactory();

Ich konnte keine ähnliche Funktionalität direkt in Unity finden; Deshalb habe ich meine eigene Erweiterung für den IUnityContainer geschrieben, mit der Sie Fabriken registrieren können, die neue Objekte auf der Grundlage von Daten aus vorhandenen Objekten erstellen, die im Wesentlichen einer Typhierarchie zugeordnet sind zu einer anderen Typhierarchie: nityMappingFactory @ GitHub

Mit dem Ziel der Einfachheit und Lesbarkeit habe ich eine Erweiterung erhalten, mit der Sie die Zuordnungen direkt angeben können, ohne einzelne Factory-Klassen oder -Schnittstellen zu deklarieren (eine Zeitersparnis). Sie fügen die Zuordnungen einfach genau dort hinzu, wo Sie die Klassen während des normalen Bootstrapping-Vorgangs registrieren ...

//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>();

Dann deklarieren Sie einfach die Mapping Factory-Schnittstelle im Konstruktor für CI und verwenden dessen Create () -Methode ...

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

Als zusätzlichen Bonus werden alle zusätzlichen Abhängigkeiten im Konstruktor der zugeordneten Klassen auch während der Objekterstellung aufgelöst.

Natürlich wird dies nicht jedes Problem lösen, aber es hat mir bisher ziemlich gute Dienste geleistet, und ich dachte, ich sollte es teilen. Es gibt mehr Dokumentation auf der Projektseite auf GitHub.

5
jigamiller

Ich denke, ich habe es gelöst und es fühlt sich ziemlich gesund an, also muss es halbwegs richtig sein :))

Ich habe IMyIntf in eine "Getter" - und eine "Setter" -Schnittstelle aufgeteilt. So:

interface IMyIntf {
  string RunTimeParam { get; }
}


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

Dann die Umsetzung:

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() kann immer noch mehrfach aufgerufen werden, aber mit Bits von Service Locator-Paradigma können wir es ganz schön zusammenfassen, so dass IMyIntfSetter fast eine interne Schnittstelle ist, die sich unterscheidet von IMyIntf.

1
Igor Zevaka

Ich kann nicht mit einer bestimmten Unity-Terminologie antworten, aber es hört sich so an, als würden Sie gerade etwas über die Abhängigkeitsinjektion lernen. Wenn ja, fordere ich Sie dringend auf, das kurze, klare und informative Benutzerhandbuch für Ninject zu lesen.

Auf diese Weise werden Sie durch die verschiedenen Optionen geführt, die Sie bei der Verwendung von DI haben, und erfahren, wie Sie die spezifischen Probleme berücksichtigen, mit denen Sie unterwegs konfrontiert sind. In Ihrem Fall möchten Sie höchstwahrscheinlich den DI-Container verwenden, um Ihre Objekte zu instanziieren, und das Objekt soll über den Konstruktor einen Verweis auf jede seiner Abhängigkeiten erhalten.

In der exemplarischen Vorgehensweise wird auch erläutert, wie Methoden, Eigenschaften und sogar Parameter mithilfe von Attributen mit Anmerkungen versehen werden, um sie zur Laufzeit zu unterscheiden.

Auch wenn Sie Ninject nicht verwenden, erhalten Sie in der exemplarischen Vorgehensweise die Konzepte und die Terminologie der Funktionen, die Ihrem Zweck entsprechen, und Sie sollten in der Lage sein, dieses Wissen Unity oder anderen DI-Frameworks zuzuordnen (oder Sie davon zu überzeugen, Ninject auszuprobieren). .

1
anthony