Umgebungskontext vs. Konstruktorinjektion

9

Ich habe viele Kernklassen, die ISessionContext der Datenbank, ILogManager für das Protokoll und IService für die Kommunikation mit anderen Diensten benötigen. Ich möchte die Abhängigkeitsinjektion für diese Klasse verwenden, die von allen Kernklassen verwendet wird.

Ich habe zwei mögliche Implementierungen. Die Kernklasse, die IAmbientContext mit allen drei Klassen akzeptiert oder für alle Klassen die drei Klassen injiziert.

public interface ISessionContext 
{
    ...
}

public class MySessionContext: ISessionContext 
{
    ...
}

public interface ILogManager 
{

}

public class MyLogManager: ILogManager 
{
    ...
}

public interface IService 
{
    ...
}

public class MyService: IService
{
    ...
}

Erste Lösung:

public class AmbientContext
{
    private ISessionContext sessionContext;
    private ILogManager logManager;
    private IService service;

    public AmbientContext(ISessionContext sessionContext, ILogManager logManager, IService service)
    {
        this.sessionContext = sessionContext;
        this.logManager = logManager;
        this.service = service;
    }
}


public class MyCoreClass(AmbientContext ambientContext)
{
    ...
}

zweite Lösung (ohne Umgebungskontext)

public MyCoreClass(ISessionContext sessionContext, ILogManager logManager, IService service)
{
    ...
}

Was ist in diesem Fall die beste Lösung?

user3401335
quelle
Was wird " IServicefür die Kommunikation mit anderen Diensten verwendet"? Wenn es sich IServiceum eine vage Abhängigkeit von anderen Diensten handelt, klingt dies wie ein Dienstfinder und sollte nicht existieren. Ihre Klasse sollte von Schnittstellen abhängen, die explizit beschreiben, was ihre Verbraucher mit ihnen tun werden. Keine Klasse benötigt jemals einen Dienst, um Zugriff auf einen Dienst zu erhalten. Eine Klasse benötigt eine Abhängigkeit, die etwas Bestimmtes tut, das die Klasse benötigt.
Scott Hannen

Antworten:

4

"Best" ist hier viel zu subjektiv. Wie bei solchen Entscheidungen üblich, handelt es sich um einen Kompromiss zwischen zwei gleichermaßen gültigen Wegen, um etwas zu erreichen.

Wenn Sie eine erstellen AmbientContextund diese in viele Klassen einfügen, stellen Sie möglicherweise jeder von ihnen mehr Informationen zur Verfügung, als sie benötigen (z. B. Fookann die Klasse nur verwenden ISessionContext, wird aber darüber informiert ILogManagerund ISessionauch).

Wenn Sie jede Klasse über einen Parameter übergeben, teilen Sie jeder Klasse nur die Dinge mit, über die sie Bescheid wissen muss. Die Anzahl der Parameter kann jedoch schnell zunehmen, und Sie haben möglicherweise viel zu viele Konstruktoren und Methoden mit vielen sich stark wiederholenden Parametern, die über eine Kontextklasse vereinfacht werden könnten.

Es geht also darum, die beiden zu balancieren und die für Ihre Umstände geeignete auszuwählen. Wenn Sie nur eine Klasse und drei Parameter haben, würde ich mich persönlich nicht darum kümmern AmbientContext. Für mich wäre der Wendepunkt wahrscheinlich vier Parameter. Aber das ist reine Meinung. Ihr Wendepunkt wird wahrscheinlich anders sein als meiner. Gehen Sie also mit dem um, was sich für Sie richtig anfühlt.

David Arno
quelle
4

Die Terminologie aus der Frage stimmt nicht wirklich mit dem Beispielcode überein. Dies Ambient Contextist ein Muster, das verwendet wird, um eine Abhängigkeit von einer Klasse in einem Modul so einfach wie möglich zu erfassen, ohne jede Klasse zu verschmutzen, um die Schnittstelle der Abhängigkeit zu akzeptieren, aber dennoch die Idee der Umkehrung der Kontrolle beizubehalten. Solche Abhängigkeiten sind normalerweise für Protokollierung, Sicherheit, Sitzungsverwaltung, Transaktionen, Caching und Prüfung vorgesehen, um Querschnittsthemen in dieser Anwendung zu berücksichtigen. Es ist irgendwie ärgerlich , Konstruktoren ein ,, hinzuzufügen ILogging, und meistens brauchen nicht alle Klassen alle gleichzeitig, also verstehe ich Ihre Bedürfnisse.ISecurityITimeProvider

Was ist, wenn sich die Lebensdauer der ISessionInstanz von der unterscheidet ILogger? Möglicherweise sollte die ISession-Instanz bei jeder Anforderung und der ILogger einmal erstellt werden. Wenn all diese Abhängigkeiten von einem Objekt gesteuert werden, das nicht der Container selbst ist, ist dies aufgrund all dieser Probleme mit der Lebensdauerverwaltung und -lokalisierung und anderen in diesem Thread beschriebenen Problemen nicht die richtige Wahl.

Das IAmbientContextin der Frage löst nicht das Problem, nicht jeden Konstruktor zu verschmutzen. Sie müssen es natürlich immer noch nur einmal in der Konstruktorsignatur verwenden.

Der einfachste Weg ist also, KEINE Konstruktorinjektion oder einen anderen Injektionsmechanismus zu verwenden, um Querschnittsabhängigkeiten zu behandeln, sondern einen statischen Aufruf zu verwenden . Wir sehen dieses Muster tatsächlich ziemlich oft, implementiert durch das Framework selbst. Überprüfen Sie Thread.CurrentPrincipal , eine statische Eigenschaft, die eine Implementierung der IPrincipalSchnittstelle zurückgibt . Es ist auch einstellbar, sodass Sie die Implementierung ändern können, wenn Sie dies möchten, sodass Sie nicht daran gebunden sind.

MyCore sieht jetzt so aus wie

public class MyCoreClass
{
    public void BusinessFeature(string data)
    {
        LoggerContext.Current.Log(data);

        _repository.SaveProcessedData();

        SessionContext.Current.SetData(data);
        ...etc
    }
}

Dieses Muster und mögliche Implementierungen wurden von Mark Seemann in diesem Artikel ausführlich beschrieben . Möglicherweise gibt es Implementierungen, die auf dem von Ihnen verwendeten IoC-Container selbst basieren.

Sie wollen vermeiden AmbientContext.Current.Logger, AmbientContext.Current.Sessionaus den gleichen Gründen wie oben beschrieben.

Sie haben jedoch andere Möglichkeiten, um dieses Problem zu lösen: Verwenden Sie Dekoratoren, dynamisches Abfangen, wenn Ihr Container über diese Funktion verfügt, oder AOP. Der Umgebungskontext sollte der letzte Ausweg sein, da seine Clients ihre Abhängigkeiten dadurch verbergen. Ich würde immer noch Ambient Context verwenden, wenn die Schnittstelle wirklich meinen Impuls nachahmt, eine statische Abhängigkeit wie DateTime.Nowoder zu verwenden, ConfigurationManager.AppSettingsund dieser Bedarf steigt ziemlich oft. Aber am Ende ist die Konstruktorinjektion möglicherweise keine so schlechte Idee, um diese allgegenwärtigen Abhängigkeiten zu erhalten.

Adrian Iftode
quelle
3

Ich würde das vermeiden AmbientContext.

Erstens, wenn die Klasse davon abhängt, AmbientContextwissen Sie nicht wirklich, was sie tut. Sie müssen sich die Verwendung dieser Abhängigkeit ansehen, um herauszufinden, welche der verschachtelten Abhängigkeiten verwendet werden. Sie können auch nicht die Anzahl der Abhängigkeiten anzeigen und feststellen, ob Ihre Klasse zu viel tut, da eine dieser Abhängigkeiten tatsächlich mehrere verschachtelte Abhängigkeiten darstellen kann.

Zweitens, wenn Sie es verwenden, um mehrere Konstruktorabhängigkeiten zu vermeiden, wird dieser Ansatz andere Entwickler (einschließlich Sie selbst) dazu ermutigen, dieser Umgebungskontextklasse neue Mitglieder hinzuzufügen. Dann verschärft sich das erste Problem.

Drittens AmbientContextist es schwieriger , sich über die Abhängigkeit zu lustig zu machen, da Sie in jedem Fall herausfinden müssen, ob Sie alle Mitglieder oder nur die Mitglieder verspotten möchten, die Sie benötigen, und dann ein Modell einrichten müssen, das diese Verspottungen (oder Testdoppel) zurückgibt Ihr Gerät ist schwieriger zu schreiben, zu lesen und zu warten.

Viertens mangelt es an Zusammenhalt und es verstößt gegen das Prinzip der Einzelverantwortung. Aus diesem Grund hat es einen Namen wie "AmbientContext", weil es viele Dinge tut, die nichts miteinander zu tun haben, und es keine Möglichkeit gibt, ihn entsprechend seiner Funktion zu benennen.

Und es verstößt wahrscheinlich gegen das Prinzip der Schnittstellentrennung, indem Schnittstellenmitglieder in Klassen eingeführt werden, die sie nicht benötigen.

Scott Hannen
quelle
2

Der zweite (ohne Interface Wrapper)

Sofern keine Interaktion zwischen den verschiedenen Diensten besteht, die in einer Zwischenklasse gekapselt werden müssen, wird Ihr Code nur kompliziert und die Flexibilität wird eingeschränkt, wenn Sie die "Schnittstelle der Schnittstellen" einführen.

Ewan
quelle