Wie verwende ich die Abhängigkeitsinjektion und vermeide zeitliche Kopplung?

11

Angenommen, ich habe die Service, die Abhängigkeiten über den Konstruktor empfängt, aber auch mit benutzerdefinierten Daten (Kontext) initialisiert werden muss, bevor sie verwendet werden können:

public interface IService
{
    void Initialize(Context context);
    void DoSomething();
    void DoOtherThing();
}

public class Service : IService
{
    private readonly object dependency1;
    private readonly object dependency2;
    private readonly object dependency3;

    public Service(
        object dependency1,
        object dependency2,
        object dependency3)
    {
        this.dependency1 = dependency1 ?? throw new ArgumentNullException(nameof(dependency1));
        this.dependency2 = dependency2 ?? throw new ArgumentNullException(nameof(dependency2));
        this.dependency3 = dependency3 ?? throw new ArgumentNullException(nameof(dependency3));
    }

    public void Initialize(Context context)
    {
        // Initialize state based on context
        // Heavy, long running operation
    }

    public void DoSomething()
    {
        // ...
    }

    public void DoOtherThing()
    {
        // ...
    }
}

public class Context
{
    public int Value1;
    public string Value2;
    public string Value3;
}

Jetzt - die Kontextdaten sind nicht im Voraus bekannt, daher kann ich sie nicht als Abhängigkeit registrieren und DI verwenden, um sie in den Dienst einzufügen

So sieht ein Beispielclient aus:

public class Client
{
    private readonly IService service;

    public Client(IService service)
    {
        this.service = service ?? throw new ArgumentNullException(nameof(service));
    }

    public void OnStartup()
    {
        service.Initialize(new Context
        {
            Value1 = 123,
            Value2 = "my data",
            Value3 = "abcd"
        });
    }

    public void Execute()
    {
        service.DoSomething();
        service.DoOtherThing();
    }
}

Wie Sie sehen können, handelt es sich um zeitliche Kopplungs- und Initialisierungsgerüche von Methodencode, da ich zuerst aufrufen muss, um aufrufen service.Initializezu können, service.DoSomethingund service.DoOtherThingdanach.

Was sind die anderen Ansätze, mit denen ich diese Probleme beseitigen kann?

Zusätzliche Klarstellung des Verhaltens:

Jede Instanz des Clients muss über eine eigene Instanz des Dienstes verfügen, die mit den spezifischen Kontextdaten des Clients initialisiert wurde. Diese Kontextdaten sind also nicht statisch oder im Voraus bekannt, sodass sie nicht von DI in den Konstruktor eingefügt werden können.

Dusan
quelle

Antworten:

17

Es gibt verschiedene Möglichkeiten, um das Initialisierungsproblem zu lösen:

  • Wie in /software//a/334994/301401 beantwortet , sind init () -Methoden ein Codegeruch. Das Initialisieren eines Objekts liegt in der Verantwortung des Konstruktors - deshalb haben wir schließlich Konstruktoren.
  • Hinzufügen Der angegebene Dienst muss mit dem Dokumentkommentar des ClientKonstruktors initialisiert werden und vom Konstruktor ausgelöst werden, wenn der Dienst nicht initialisiert wird. Dies überträgt die Verantwortung auf denjenigen, der Ihnen das IServiceObjekt gibt.

In Ihrem Beispiel Clientist dies jedoch der einzige, der die Werte kennt, an die übergeben wird Initialize(). Wenn Sie es so halten möchten, würde ich Folgendes vorschlagen:

  • Fügen Sie ein hinzu IServiceFactoryund übergeben Sie es an den ClientKonstruktor. Dann können Sie anrufen, serviceFactory.createService(new Context(...))wodurch Sie eine Initialisierung erhalten IService, die von Ihrem Client verwendet werden kann.

Die Fabriken können sehr einfach sein und es Ihnen auch ermöglichen, init () -Methoden zu vermeiden und stattdessen Konstruktoren zu verwenden:

public interface IServiceFactory
{
    IService createService(Context context);
}

public class ServiceFactory : IServiceFactory
{
    public Service createService(Context context)
    {
        return new Service(context);
    }
}

Im Client OnStartup()ist auch eine Initialisierungsmethode (es wird nur ein anderer Name verwendet). Wenn möglich (wenn Sie die ContextDaten kennen), sollte die Factory direkt im ClientKonstruktor aufgerufen werden . Wenn dies nicht möglich ist, müssen Sie das speichern IServiceFactoryund aufrufen OnStartup().

Wenn ServiceAbhängigkeiten nicht von bereitgestellt werden Client, werden sie von DI bereitgestellt durch ServiceFactory:

public interface IServiceFactory
{
    IService createService(Context context);
}    

public class ServiceFactory : IServiceFactory
{        
    private readonly object dependency1;
    private readonly object dependency2;
    private readonly object dependency3;

    public ServiceFactory(object dependency1, object dependency2, object dependency3)
    {
        this.dependency1 = dependency1;
        this.dependency2 = dependency2;
        this.dependency3 = dependency3;
    }

    public Service createService(Context context)
    {
        return new Service(context, dependency1, dependency2, dependency3);
    }
}
pschill
quelle
1
Vielen Dank, genau wie ich dachte, im letzten Punkt ... Und würden Sie in der ServiceFactory den Konstruktor DI in der Factory selbst für die Abhängigkeiten verwenden, die für den Servicekonstruktor benötigt werden, oder der Service Locator wäre besser geeignet?
Dusan
1
@ Dusan verwendet keinen Service Locator. Wenn Serviceandere Abhängigkeiten als die vorhanden Contextsind, die von der nicht bereitgestellt würden Client, können sie über DI an die bereitgestellt ServiceFactorywerden, die an den ServiceZeitpunkt übergeben werden sollen, an dem createServiceaufgerufen wird.
Mr. Mindor
@Dusan Wenn Sie verschiedene Abhängigkeiten für verschiedene Dienste bereitstellen müssen (dh diese benötigt Abhängigkeit1_1, aber die nächste benötigt Abhängigkeit1_2), aber wenn dieses Muster ansonsten für Sie funktioniert, können Sie ein ähnliches Muster verwenden, das häufig als Builder-Muster bezeichnet wird. Mit einem Builder können Sie bei Bedarf im Laufe der Zeit stückweise ein Objekt einrichten. Dann können Sie dies tun ... ServiceBuilder partial = new ServiceBuilder().dependency1(dependency1_1).dependency2(dependency2_1).dependency3(dependency3_1);und mit Ihrem teilweise eingerichteten Dienst zurückbleiben, dann späterService s = partial.context(context).build()
Aaron
1

Die InitializeMethode sollte von der IServiceSchnittstelle entfernt werden, da dies ein Implementierungsdetail ist. Definieren Sie stattdessen eine andere Klasse, die die konkrete Instanz von Service verwendet und die Initialisierungsmethode aufruft. Dann implementiert diese neue Klasse die IService-Schnittstelle:

public class ContextDependentService : IService
{
    public ContextDependentService(Context context, Service service)
    {
        this.service = service;

        service.Initialize(context);
    }

    // Methods in the IService interface
}

Dadurch wird der Clientcode über die Initialisierungsprozedur nicht informiert, es sei denn, die ContextDependentServiceKlasse wird initialisiert. Sie beschränken zumindest die Teile Ihrer Anwendung, die über dieses Wonky-Initialisierungsverfahren Bescheid wissen müssen.

Greg Burghardt
quelle
1

Es scheint mir, dass Sie hier zwei Möglichkeiten haben

  1. Verschieben Sie den Initialisierungscode in den Kontext und fügen Sie einen initialisierten Kontext ein

z.B.

public InitialisedContext Initialise()
  1. Führen Sie den ersten Aufruf aus, um die Anrufinitialisierung auszuführen, falls dies noch nicht geschehen ist

z.B.

public async Task Execute()
{
     //lock context
     //check context is not initialised
     // init if required
     //execute code...
}
  1. Wirf einfach Ausnahmen aus, wenn der Kontext beim Aufruf von Execute nicht initialisiert wird. Wie SqlConnection.

Das Injizieren einer Factory ist in Ordnung, wenn Sie nur vermeiden möchten, den Kontext als Parameter zu übergeben. Angenommen, nur diese bestimmte Implementierung benötigt einen Kontext und Sie möchten ihn nicht zur Schnittstelle hinzufügen

Aber Sie haben im Wesentlichen das gleiche Problem: Was ist, wenn die Fabrik noch keinen initialisierten Kontext hat?

Ewan
quelle
0

Sie sollten Ihre Schnittstelle nicht von einem Datenbankkontext und einer Initialisierungsmethode abhängig machen. Sie können dies im konkreten Klassenkonstruktor tun.

public interface IService
{
    void DoSomething();
    void DoOtherThing();
}

public class Service : IService
{
    private readonly object dependency1;
    private readonly object dependency2;
    private readonly object dependency3;
    private readonly object context;

    public Service(
        object dependency1,
        object dependency2,
        object dependency3,
        object context )
    {
        this.dependency1 = dependency1 ?? throw new ArgumentNullException(nameof(dependency1));
        this.dependency2 = dependency2 ?? throw new ArgumentNullException(nameof(dependency2));
        this.dependency3 = dependency3 ?? throw new ArgumentNullException(nameof(dependency3));

        // context is concrete class details not interfaces.
        this.context = context;

        // call init here constructor.
        this.Initialize(context);
    }

    protected void Initialize(Context context)
    {
        // Initialize state based on context
        // Heavy, long running operation
    }

    public void DoSomething()
    {
        // ...
    }

    public void DoOtherThing()
    {
        // ...
    }
}

Eine Antwort auf Ihre Hauptfrage wäre Property Injection .

public class Service
    {
        public Service(Context context)
        {
            this.context = context;
        }

        private Dependency1 _dependency1;
        public Dependency1 Dependency1
        {
            get
            {
                if (_dependency1 == null)
                    _dependency1 = Container.Resolve<Dependency1>();

                return _dependency1;
            }
        }

        //...
    }

Auf diese Weise können Sie alle Abhängigkeiten per Eigenschaftsinjektion aufrufen . Aber es könnte eine riesige Zahl sein. In diesem Fall können Sie Constructor Injection für sie verwenden, aber Sie können Ihren Kontext anhand der Eigenschaft festlegen, indem Sie überprüfen, ob er null ist.

Engineert
quelle
OK, großartig, aber ... für jede Instanz des Clients muss eine eigene Instanz des Dienstes mit unterschiedlichen Kontextdaten initialisiert werden. Diese Kontextdaten sind nicht statisch oder vorher bekannt, sodass sie nicht von DI in den Konstruktor eingefügt werden können. Wie erhalte / erstelle ich dann eine Instanz des Dienstes zusammen mit anderen Abhängigkeiten in meinen Clients?
Dusan
hmm wird dieser statische Konstruktor nicht ausgeführt, bevor Sie den Kontext festlegen? und initialisieren im Konstruktor Risiken Ausnahmen
Ewan
Ich neige dazu, Factory zu injizieren, die den Service mit den angegebenen Kontextdaten erstellen und initialisieren kann (anstatt den Service selbst zu injizieren), bin mir aber nicht sicher, ob es bessere Lösungen gibt.
Dusan
@Ewan Du hast recht. Ich werde versuchen, eine Lösung dafür zu finden. Aber vorher werde ich es vorerst entfernen.
Engineert
0

Misko Hevery hat einen sehr hilfreichen Blog-Beitrag über den Fall, mit dem Sie konfrontiert sind. Sie benötigen beide neue und injizierbare Elemente für Ihre ServiceKlasse, und dieser Blog-Beitrag kann Ihnen helfen.

Fettdruck P.
quelle