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

147

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

Im Moment kann ich mir nur vorstellen, wie das geht, wenn ich eine Init-Methode auf der Schnittstelle habe.

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

Um es dann (in Unity) zu verwenden, würde ich Folgendes tun:

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

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

Dies führt zu einer Reihe von Problemen, nämlich dass die InitializeMethode auf der Schnittstelle verfügbar ist und mehrfach aufgerufen werden kann. Das Setzen eines Flags in der Implementierung und das Auslösen einer Ausnahme bei wiederholtem Aufruf von Initializescheint sehr umständlich.

An dem Punkt, an dem ich meine Schnittstelle auflöse, möchte ich nichts über die Implementierung von wissen IMyIntf. 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 irgendwie zu kommentieren (Attribute?) Und diese beim Erstellen des Objekts an das Framework zu übergeben?

Bearbeiten: Beschrieb die Oberfläche etwas mehr.

Igor Zevaka
quelle
9
Es fehlt Ihnen der Sinn, einen DI-Container zu verwenden. Abhängigkeiten sollen für Sie gelöst werden.
Pierreten
Woher beziehen Sie Ihre benötigten Parameter? (Konfigurationsdatei, db, ??)
Jaime
runTimeParamist eine Abhängigkeit, die zur Laufzeit anhand einer Benutzereingabe ermittelt wird. Sollte die Alternative dazu darin bestehen, es in zwei Schnittstellen aufzuteilen - eine zur Initialisierung und eine zum Speichern von Werten?
Igor Zevaka
Die Abhängigkeit in IoC bezieht sich normalerweise auf die Abhängigkeit von anderen Ref-Typ-Klassen oder -Objekten, die in der IoC-Initialisierungsphase bestimmt werden können. Wenn Ihre Klasse nur einige Werte benötigt, um zu funktionieren, ist die Initialize () -Methode in Ihrer Klasse hilfreich.
Das Licht
Ich meine, stellen Sie sich vor, Ihre App enthält 100 Klassen, auf die dieser Ansatz angewendet werden kann. Dann müssen Sie zusätzliche 100 Factory-Klassen + 100 Schnittstellen für Ihre Klassen erstellen, und Sie könnten damit durchkommen, wenn Sie nur die Initialize () -Methode verwenden.
Das Licht

Antworten:

276

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

Initialisierungsmethoden auf den Schnittstellen riechen nach einer undichten Abstraktion .

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

Daher sollte die Schnittstelle 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 erstellen IMyIntfFactory, die konkrete Instanzen IMyIntfwie 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 wir so die Invarianten der Klasse mithilfe des readonlySchlüsselworts schützen können . Es sind keine stinkenden Initialisierungsmethoden erforderlich.

Eine IMyIntfFactoryImplementierung kann so einfach sein:

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

In all Ihren Verbrauchern, in denen Sie eine IMyIntfInstanz benötigen , nehmen Sie einfach eine Abhängigkeit davon auf, IMyIntfFactoryindem Sie sie über Constructor Injection anfordern .

Jeder DI-Container, der sein Geld wert ist, kann eine IMyIntfFactoryInstanz automatisch für Sie verkabeln, wenn Sie sie korrekt registrieren.

Mark Seemann
quelle
13
Das Problem ist, dass eine Methode (wie Initialize) Teil Ihrer API ist, der Konstruktor jedoch nicht. blog.ploeh.dk/2011/02/28/InterfacesAreAccessModifiers.aspx
Mark Seemann
12
Darüber hinaus zeigt eine Initialisierungsmethode die zeitliche Kopplung an: blog.ploeh.dk/2011/05/24/DesignSmellTemporalCoupling.aspx
Mark Seemann
2
@Darlene Möglicherweise können Sie einen träge initialisierten Decorator verwenden, wie in Abschnitt 8.3.6 meines Buches beschrieben . Ich gebe auch ein Beispiel für etwas Ähnliches in meiner Präsentation Big Object Graphs Up Front .
Mark Seemann
2
@Mark Wenn für die Erstellung der MyIntfImplementierung durch die Factory mehr als erforderlich ist runTimeParam(lesen Sie: andere Dienste, die von einem IoC aufgelöst werden sollen), müssen Sie diese Abhängigkeiten in Ihrer Factory weiterhin auflösen. Ich mag die Antwort von @PhilSandler, diese Abhängigkeiten an den Konstruktor der Fabrik zu übergeben , um dies zu lösen - ist das auch Ihre Meinung dazu?
Jeff
2
Auch tolles Zeug, aber deine Antwort auf diese andere Frage kam wirklich zu meinem Punkt.
Jeff
15

Wenn Sie auf diese Situation stoßen, müssen Sie normalerweise Ihr Design überdenken und feststellen, ob Sie Ihre Stateful- / Datenobjekte mit Ihren reinen Diensten mischen. In den meisten (nicht allen) Fällen sollten Sie diese beiden Objekttypen getrennt halten.

Wenn Sie einen kontextspezifischen Parameter benötigen, der im Konstruktor übergeben wird, 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 () verwendet. ), Build () oder wie auch immer Sie Ihre Factory-Methoden nennen).

Setter oder eine Initialize () -Methode werden im Allgemeinen als schlechtes Design angesehen, da Sie sich "daran erinnern" müssen, sie aufzurufen und sicherzustellen, dass sie nicht zu viel vom Status Ihrer Implementierung preisgeben (dh was jemanden davon abhält, erneut zu arbeiten) -Calling Initialize oder der Setter?).

Phil Sandler
quelle
5

Ich bin auch einige Male auf diese Situation in Umgebungen gestoßen, in denen ich dynamisch ViewModel-Objekte basierend auf Modellobjekten erstelle (in diesem anderen Stackoverflow-Beitrag sehr gut beschrieben ).

Mir hat gefallen, wie die Ninject-Erweiterung es Ihnen 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 basierend auf Daten von vorhandenen Objekten erstellen, die im Wesentlichen einer Typhierarchie einer anderen Typhierarchie zugeordnet sind : UnityMappingFactory @ 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 (ein Echtzeit-Sparer). Sie fügen die Zuordnungen einfach genau dort hinzu, wo Sie die Klassen während des normalen Bootstrapping-Prozesses 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 die 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, so dass ich dachte, ich sollte es teilen. Weitere Dokumentationen finden Sie auf der Projektseite auf GitHub.

Jigamiller
quelle
1

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

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

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.

Selbst 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). .

Anthony
quelle
Dank dafür. Ich evaluiere gerade DI-Frameworks und NInject sollte mein nächstes sein.
Igor Zevaka
@johann: Anbieter? github.com/ninject/ninject/wiki/…
Anthony
1

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

Ich habe mich IMyIntfin eine "Getter" - und eine "Setter" -Schnittstelle aufgeteilt. So:

interface IMyIntf {
  string RunTimeParam { get; }
}


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

Dann die Implementierung:

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 mehrmals aufgerufen werden, aber mit Teilen des Service Locator-Paradigmas können wir es ganz gut zusammenfassen, so dass IMyIntfSetteres sich fast um eine interne Schnittstelle handelt, die sich von dieser unterscheidet IMyIntf.

Igor Zevaka
quelle
13
Dies ist keine besonders gute Lösung, da sie auf einer Initialisierungsmethode basiert, bei der es sich um eine undichte Abstraktion handelt. Übrigens sieht dies nicht nach Service Locator aus, sondern eher nach Interface Injection. In jedem Fall finden Sie in meiner Antwort eine bessere Lösung.
Mark Seemann