Laden...

[IoC] Strengere Trennung zwischen ServiceLocator und DI-Container?

Erstellt von winSharp93 vor 14 Jahren Letzter Beitrag vor 13 Jahren 4.461 Views
winSharp93 Themenstarter:in
5.742 Beiträge seit 2007
vor 14 Jahren
[IoC] Strengere Trennung zwischen ServiceLocator und DI-Container?

Hallo zusammen,

dieser Thread kann gewissermaßen als Fortführung meiner Gedanken aus IoC: Aufgelöste Objekte oder DI-Container übergeben? angesehen werden.

Der "klassische" ServiceLocator sieht ja meist im Wesentlichen so aus:


public interface IServiceLocator
{
    T Resolve<T>();
}

Die Verwendung ist denkbar einfach, wenn ich z.B. eine konkrete Repräsentation von ICustomer möchte, kann ich das mithilfe von


ICustomer customer = serviceLocator.Resolve<ICustomer>();

errreichen.
Ähnlich verhält es sich z.B. mit einem ICustomerRepository:


ICustomerRepository repository = serviceLocator.Resolve<ICustomerRepository>();

Doch bei genauerer Betrachtung unterscheiden sich die beiden Codezeilen doch stärker voneinander als man auf den ersten Blick erwartet.
Nehmen wir einmal an, der ServiceLocator wurde wie folgt initialisiert:


locator.RegisterCreation<ICustomer>(() => new Customer());
locator.RegisterSingleton<ICustomerRepository>(new CustomerRepository());

Während also bei jedem Aufruf von serviceLocator.Resolve<ICustomer>() ein neuer Customer erzeugt wird, liefert serviceLocator.Resolve<ICustomerRepository>() immer dasselbe Objekt.

Das ist für mich irgendwie inakzeptabel. "Richtig" bzw. "logisch" wäre ja, dass ein ServiceLocator nur Singletons liefert - nicht umsonst heißt die Methode ja "Resolve" und nicht etwa "Create" oder "New".

Im Gegensatz hierzu sollte ein DI-Container konsequenterweise immer neue Objekte liefern und nie Singletons; seine "Erzeugungsmethode" sollte folglich also auch nicht Resolve heißen. Allerdings bin ich bisher noch auf keinen DI-Container gestoßen, der nicht auch die Möglichkeit bietet, Singletons zu erzeugen.

Um auf IoC: Aufgelöste Objekte oder DI-Container übergeben? zurückzukommen:
Meine jetzige Idee wäre nun also, im Normalfall einen ServiceLocator injizieren zu lassen, aber keinen DI-Container. Falls letzterer tatsächlich benötigt werden würde, könnte man ihn ja ohne Weiteres über den ServiceLocator anfordern, auf den ICustomer bezogen also wie folgt:


ICustomer customer = this.ServiceLocator.Resolve<IDIContainer>().Create<ICustomer>();

Ansonsten würde man auch sämtliche Objekte, die für Aggregationen benötigt werden, per Constructorinjection injizieren lassen - und nicht, wie ich in dem genannten Thread überlegt hatte, danach vom injizierten ServiceLocator aufbauen lassen. Das ist ja aufgrund der vorherigen Argumentation auch gar nicht Aufgabe des ServiceLocators - man müsste erst einmal einen DI-Container auflösen lassen. Das wiederum ist innerhalb des Konstruktors IMHO keine gute Idee.

Der DI-Container wäre nun auch derjenige, der tatsächlich Parameter für Objekterzeugungen akzeptieren könnte: Ob diese nun vollständig oder nur teilweise angegeben werden und die restlichen der DI-Container selbst organisiert, sollte dabei keine Rolle spielen.

Aber auch hier stößt man wieder auf logische Probleme: Es existieren sowohl "Arten" von Konstruktorparametern, bei denen es Sinn macht, sie von Erzeugung zu Erzeugung zu variieren und solche, bei denen das keinen Sinn macht.
Als Beispiel wieder der ICustomer: Angenommen, der Konstruktor der tatsächlichen Instanz sieht folgendermaßen aus:


public Customer(IServiceLocator serviceLocator, int id) { //... } 

Der erste Parameter wurde ja vorhin schon beleuchtet, was ist aber mit dem zweiten?
Es macht schlichtweg keinen Sinn, eine ID (die sich während der Lebenszeit unseres _Customer_s nicht ändern soll), auch von einem DI-Container injizieren zu lassen; ein Integer ist hierfür einfach zu "primitiv".
Es existieren also gewissermaßen zwei Arten von Abhängigkeiten eines Objektes, die ihm während der Erzeugung mitgegeben werden müssen: solche Abhängigkeiten, die eher in Richtung "Infrastruktur" eingeordnet werden können, und solche, die tatsächlich den Zustand des Objektes nach der Erzeugung beeinflussen.

Eigentlich existiert ja sogar noch eine dritte Art von Abhängigkeiten; nämlich solche, die sich zur Laufzeit ändern können, aber für alle Objekte eines Typs gleich sind. Diese könnte man dann z.B. mithilfe des MEF (Managed Extensibility Frameworks) injizieren - zu diesem Zweck kann ich mich auch mit diesem anfreunden. Ob darunter nun auch der ServiceLocator fällt, ist IMHO offen.

Man muss die "Abhängigkeitsparameter" folglich dem DI-Container irgendwie mitteilen können.
Um dabei jedoch nicht "blind" vorgehen zu müssen, indem man z.B. den DI-Container wie folgt aufbaut:


public interface IContainer
{
    T Create<T>(params object[] args);
}

//...
ICustomer customer = container.Create<ICustomer>(5);

müsste man ICustomer nun eigentlich dahingehend erweitern, dass es in gewisser Hinsicht auch weiß, wie es erzeugt werden muss bzw. welche Aghängigkeiten bereits bei der Erzeugung aufgelöst übergeben werden müssen.
Da C# hier (leider?) keine Möglichkeit bietet, könnte man auf eine Art "Hilfskonstrukt" ausweichen:


[ConstructionParameters(typeof(CustomerParameters))]
public interface ICustomer
{
   //...
}

public struct CustomerParameters
   : IConstructionParameters<ICustomer>
{
    public int Id
    {
        get;
        set;
    }
}

Dann könnte man mit verbesserter Typsicherheit schreiben


public interface IContainer
{
    T Create<T>(IConstructionParameters<T> parameters);
}
//...
ICustomer customer = container.Create<ICustomer>(new CustomerParameters { Id = 5 });

Allerdings weiß ich ehrlich gesagt noch nicht wirklich, was ich von dieser Idee halten soll 🤔
Eine striktere Trennung zwischen DI-Containern und ServiceLocatorn, als ich sie bisher praktiziert habe, scheint mir allerdings inzwischen fast unabdingbar.

Was meint ihr?

winSharp93 Themenstarter:in
5.742 Beiträge seit 2007
vor 14 Jahren

Hmm - doch so viele Antworten?^^

5.942 Beiträge seit 2005
vor 14 Jahren

Hallo winSharp83

Leider ist in den alten Versionen der DI-Container noch Singleton als Standard definiert. In den neuen ist es meistens Transient, also das du standardmässig jedesmal eine neuen Instanz erhältst.

Ich finde das auch besser so.

Die Geschichte mit den Argumenten finde ich zu umständlich, zumal du im Container auch einen guten Algorithmus finden kannst, der die Parameter so zusammensetzt, wie es nötig ist.

In der neuen Version von LightCore ist das mit Argumenten und Laufzeitargumenten gelöst, wobei die Laufzeitargumente höher priorisiert sind.

Das funktioniert eigentlich wunderbar 😃

Gruss Peter

--
Microsoft MVP - Visual Developer ASP / ASP.NET, Switzerland 2007 - 2011

winSharp93 Themenstarter:in
5.742 Beiträge seit 2007
vor 14 Jahren

Das ging jetzt schnell - danke für die Antwort 🙂

BTW: Habe ich das einfach nur übersehen oder findet sich auf deiner Hompage tatsächlich kein Link zur Lightcore Homepage? Wenn nein: Da sollte unbedingt einer hin 🙂

Die Geschichte mit den Argumenten finde ich zu umständlich, zumal du im Container auch einen guten Algorithmus finden kannst, der die Parameter so zusammensetzt, wie es nötig ist.

Mir ging es in diesem Punkt auch eher um die Fragen:*Sollte ein über einen DI-Container erzeugtes Objekt überhaupt Konstruktorparameter erwarten? *Wenn ja: Wer legt diese fest? *Und: Wer prüft deren Korrektheit zur Compilezeit bzw. auch zur Laufzeit.

Da wäre ja man gewissermaßen wieder bei der Frage: Sollten Schnittstellen auch die Möglichkeit bieten, Konstruktoren anzubieten?
Hatten wir dazu nicht sogar mal irgendwo eine Diskussion? 🤔

Ein Interface soll ja gerade unabhängig von der konkreten Implementierung sein:


ICustomer customer = container.Instantiate<ICustomer>(5);

Ist das noch Unabhängigkeit von der Implementierung?
Ich sage: Nein!
Denn:


public sealed class CustomerImplA
{
   public CustomerImplA(int id) { /*...*/ }
}
public sealed class CustomerImplB
{
   public CustomerImplA(int id, int age) { /*...*/ }
}
public sealed class CustomerImplC
{
   public CustomerImplA() { /*...*/ }
}

Wenn man dann im Nachhinein CustomerImplA durch eine der anderen ersetzt, knallt es zur Laufzeit.

Andererseits gebe ich zu, dass die Variante mit den Structs vielleicht tatsächlich etwas übertrieben ist.
Aber einfach nur die Parameter in ein object[] - irgendwie gefällt mir das überhaupt nicht...
Einerseits verwendet man eine streng-typisierte Sprache und dann so etwas?!?

Dann vielleicht einen Ansatz wie in CastleWindsor (ein wenig erweitert):


[ConstructionParameter("Id", typeof(int))]
public interface ICustomer
{
   //...
}
//...
public sealed class Customer : ICustomer
{
    public Customer([Parameter("Id") int id) { /*...*/ }
}
//...
ICustomer customer = container.Instantiate<ICustomer>(new { Id = 5 } );

Der Container könnte dann prüfen, ob a) alle benötigten Parameter angegeben wurden und b) keine Parameter, die die Schnittstelle nicht explizit vorschreibt.

5.942 Beiträge seit 2005
vor 14 Jahren

Hallo winSharp93

Ich würde jetzt einfach mal davon ausgehen, das der Benutzer des Interfaces weiss, wie die Implementation auszusehen hat.
Du hast schon Recht mit der Unabhängigkeit, allerdings hast du mit einem expliziten Ansatz ein Zwang zur Angaben von Parametern.

Wenn du bspw. auf einen anderne Container umsteigst, kannst du erst mal deine komplette Codebase ändern, wegen den ganzen Attributen.

Ich finde das zu viel Aufwand für einen künstlichen Kontrakt, bzw. ich denke das ist für andere zuviel Aufwand.

Gruss Peter

--
Microsoft MVP - Visual Developer ASP / ASP.NET, Switzerland 2007 - 2011

4.207 Beiträge seit 2003
vor 14 Jahren

Prinzipiell stimmt es IMHO schon, dass es dem Interface-Prinzip widerspricht, wenn man Konstruktorparameter übergeben kann.

ABER: Angenommen, es ginge nicht, wie unpraktisch wäre das? Es gibt ja Typen, die nun mal Konstruktorparameter erfordern - das hieße, solche könnte man nicht mehr erzeugen ...

Von daher sehe ich das an dieser Stelle (ausnahmsweise) mal ganz pragmatisch und denke, dass die Übergabe von Konstruktorparametern trotz Interface okay ist.

Wissensvermittler und Technologieberater
für .NET, Codequalität und agile Methoden

www.goloroden.de
www.des-eisbaeren-blog.de

winSharp93 Themenstarter:in
5.742 Beiträge seit 2007
vor 14 Jahren

Du hast schon Recht mit der Unabhängigkeit, allerdings hast du mit einem expliziten Ansatz ein Zwang zur Angaben von Parametern.

Aber ist der nicht auch notwendig (dieser Zwang)?

Denn sobald auch nur eine konkrete Implementierung einer Schnittelle (die für einen Einsatz in einem DI-Container infrage kommt) zwingend einen bestimmten Konstruktorparameter erwartet, müsste man diesen ja dem Container theoretisch immer übergeben.
Ansonsten ist die Entkoppelung durch ein Interface ja eher "scheinheilig", wenn man eigentlich bei der Erzeugung durch den DI-Container genau eine konkrete Implementierung im Sinn hat.

Wenn du bspw. auf einen anderne Container umsteigst, kannst du erst mal deine komplette Codebase ändern, wegen den ganzen Attributen.

Das sehe ich jetzt nicht als direktes Problem: Die Attribute würden einfach ignoriert werden.
Und es geht mir ja auch mehr um die Theorie - in der Praxis kann man ja noch genügend Abstriche machen 😁
Alternativ könnte man bei den Konstruktorparameterattributen ja auch auf XML ausweichen - denn IMHO reicht jedoch Doku alleine nicht.

Prinzipiell stimmt es IMHO schon, dass es dem Interface-Prinzip widerspricht, wenn man Konstruktorparameter übergeben kann.

Die Frage ist doch: Sollte man dann den Begriff "Interface" im DI-Zeitalter dann nicht dahingehend etwas erweitern?
Ein Interface auch in gewissem Grade über Umstände der Erzeugung konkreter Implementierungen bestimmen zu lassen, halte ich inzwischen gar nicht mehr für so falsch.

Es gibt ja - wie gesagt - zwei verschiedene "Arten" von Parametern an dieser Stelle: Die, die für alle möglichen Implementierungen von Interesse sind und die, welche nur für bestimmte Implementierungen relevant sind (und folglich dann also nicht auf "Interfaceebene" vereinbart würden, sondern tatsächlich vom DI-Container aufgelöst würden).
Ein Beispiel für "etwas dazwischen" (also Parameter, für die es Sinn macht, sie je nach Erzeugungsort entweder explizit zu übergeben oder vom DI-Container auflösen zu lassen) fällt mir jetzt nicht ein - ich wage einfach mal, die These aufzustellen, dass es so etwas auch nicht gibt.

4.207 Beiträge seit 2003
vor 14 Jahren

Das Problem ist, dass bei DI ein Interface so genutzt wird, wie ein syntaktisches Template - genau das ist ein Interface aber halt eben NICHT in der OOP. Von daher wäre eher ein anderes Sprachkonstrukt potenziell von Interesse, um so etwas umzusetzen.

Erweitert man Interfaces um die bestehenden Dinge, verwässert das IMHO die Definition dessen, was ein Interfaces auszeichnet.

Wissensvermittler und Technologieberater
für .NET, Codequalität und agile Methoden

www.goloroden.de
www.des-eisbaeren-blog.de

winSharp93 Themenstarter:in
5.742 Beiträge seit 2007
vor 14 Jahren

Das Problem ist, dass bei DI ein Interface so genutzt wird, wie ein syntaktisches Template - genau das ist ein Interface aber halt eben NICHT in der OOP.

Wo liegt für dich der Unterschied zwischen "Interface" und "syntaktischem Template"?
Für mich sind beide Begriffe - wenn überhaupt - nur sehr schwammig unterscheidbar.

5.942 Beiträge seit 2005
vor 14 Jahren

Hallo winSharp93

Ich denke das ich deinen Gedankengängen folgen kann.

Wenn du mit einem IoC-Container und Laufzeitargumenten arbeitest, gibt es IMHO zwei Fälle:

  • Entweder fixierst du dich wirklich nur auf einen konkreten Typen und übergibts so die Argumente. Das ist natürlich schlecht und der Container bringt nicht viel mehr als eine vorhersehbare Factory.

  • Jede Implementation zu einem Kontrakt hat gleiche Konstruktorargumente, zumindest zum Teil.
    D.h. es wird bspw. immer ein String "configurationPath" erwartet, danach, vorher oder dazwischen können noch andere registrierte Abhängigkeiten folgen, jedoch ist dieses Argument in jeder möglichen Implementation drin.

Auf diese Art der Nutzung bist du wieder total unabhängig.
Aber die Geschichte ist natürlich implizit und dein Vorschlag wäre halt, das explizit zu machen.

Ich weiss nicht ob das Sinn macht, wegen oben genannten Gründen.
Wenn dann als zusätzlich Option, aber das bläht alles wieder auf.

Wenn man also mit dem richtigen Gedanken dran geht, sind die Laufzeitargumente IMHO wirklich legitim.

Evt. hat Golo das mit dem Syntaktischen Template gemeint, eben die implizite Vorgabe im Konstruktor für alle Möglichen Implementationen.

Gruss Peter

--
Microsoft MVP - Visual Developer ASP / ASP.NET, Switzerland 2007 - 2011

4.207 Beiträge seit 2003
vor 14 Jahren

Ich würde es wie folgt definieren:

  • Ein syntaktisches Template gibt die Syntax vor, mit der ich eine Klasse verwenden kann. Dort könnten dann zB auch bestimmte Konstruktoren enthalten sein, statische Methoden oder bestimmte Felder - so ein Sprachmerkmal gibt es heute aber nicht.

  • Ein Interface ist quasi ein Stellvertreter für eine Klasse, eine Abstraktion, die von der konkreten Implementierung abstrahiert. Da ich das Interface nutzen kann, ohne die konkrete Klasse überhaupt zu kennen, machen zB statische Member in Interfaces schlichtweg keinen Sinn. Felder sind ein Implementierungsdetail und deshalb nicht in Interfaces enthalten. Und Konstruktoren erlauben die Konstruktion eines Objekts - aber auch dazu muss ich einen konkreten Typen in der Hand haben, der sich instanziieren lässt, was wiederum bei Interfaces nicht gegeben ist.

Der Punkt ist also, dass Interfaces nur solche Dinge enthalten können, die ohne Kenntnis des konkreten Typs verfügbar sind.

Wissensvermittler und Technologieberater
für .NET, Codequalität und agile Methoden

www.goloroden.de
www.des-eisbaeren-blog.de

winSharp93 Themenstarter:in
5.742 Beiträge seit 2007
vor 13 Jahren

Im Moment bin ich gerade wieder in einer Situation, wo ich eigentlich kurz davor bin, auf benannte Registrierungen in einem DI-Container zurückgreifen zu wollen.

Hintergrund ist:
Ich habe ein Interface, das verschiedene Typen zur Darstellung im UI zusammenfasst:


public interface IItem
{
    string Name { get; }
}

Jetzt gibt es z.B.:


public sealed class CustomerItem
    : IItem
{
    private readonly ICustomer _customer;

    public string Name
    {
        get { return this._customer.LastName; }
    }

    public CustomerItem(ICustomer customer)
    {
        this._customer = customer;
    }
}

public sealed class EmployeeItem : IItem
{
    private readonly IEmployee _employee;

    public string Name
    {
        get { return this._employee.Id.ToString(); }
    }

    public EmployeeItem(IEmployee employee)
    {
        this._employee = employee;
    }
}

Erzeugt werden sollen die Items alle mithilfe eines DI-Containers.

Folge ich nun dem Rat von Golo Roden, erstelle ich nun also zwei weitere Schnittstellen:


public interface IEmployeeItem : IItem { }
public interface ICustomerItem : IItem { }

und schreibe dann zur Erzeugung:


IItem item = container.Create<IEmployeeItem>(employee);
//oder
IItem item = container.Create<ICustomerItem>(customer);

Mal etwas provokant gefragt: Da kann ich doch auch gleich meinen oberen Ansatz durchführen.
Ob ich nun Interfaces oder Klassen erstelle, kommt letztendlich doch auf's selbe heraus.

Und mit leicht geänderter Syntax:


public interface IContract<T> where T : class { }

steht dann an der Stelle von IEmployeeItem ein:


public sealed class EmployeeItemContract : IContract<IItem>
{
    public IEmployee Employee { get; set; }
}
//bzw. an der Stelle von ICustomerItem
public sealed class CustomerItemContract : IContract<IItem>
{
    public ICustomerCustomer{ get; set; }
}

Bei der Erzeugung kann ich dann schreiben:


IItem item = container.Create(new EmployeeItemContract
{
    Employee = employee
});
//bzw.
IItem item = container.Create(new CustomerItemContract
{
    Customer = customer
});

Somit habe ich mir dann ja eine Art "syntaktisches Template" geschaffen.

Klar, ohne Anpassungen am DI-Container kann man das nicht so ohne weiteres realisieren, aber ich würde sagen, dass die Variante durchaus Potential hat - falls also jemand mitlesen sollte, der rein zufällig gerade einen DI-Container entwickelt, sehe derjenige das als Syntaxvorschlag 😁