Come usare l'iniezione di dipendenza ed evitare l'accoppiamento temporale?


11

Supponiamo di avere quello Serviceche riceve dipendenze tramite il costruttore ma deve anche essere inizializzato con dati personalizzati (contesto) prima che possano essere utilizzati:

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

Ora - i dati di contesto non sono noti in anticipo, quindi non posso registrarli come dipendenza e utilizzare DI per iniettarli nel servizio

Ecco come appare il client di esempio:

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

Come puoi vedere, ci sono accoppiamenti temporali e inizializzazione di odori di codice di metodo coinvolti, perché ho prima bisogno di chiamare service.Initializeper poter chiamare service.DoSomethinge service.DoOtherThingpoi.

Quali sono gli altri approcci in cui posso eliminare questi problemi?

Ulteriori chiarimenti sul comportamento:

Ogni istanza del client deve avere la propria istanza del servizio inizializzata con i dati di contesto specifici del client. Pertanto, i dati di contesto non sono statici o noti in anticipo, quindi non possono essere iniettati da DI nel costruttore.

Risposte:


18

Esistono diversi modi per affrontare il problema di inizializzazione:

  • Come spiegato in /software//a/334994/301401 , i metodi init () sono un odore di codice. L'inizializzazione di un oggetto è responsabilità del costruttore - ecco perché abbiamo costruttori dopo tutto.
  • Aggiungi Il servizio indicato deve essere inizializzato al commento doc del Clientcostruttore e lasciare che il costruttore venga lanciato se il servizio non è inizializzato. Questo sposta la responsabilità verso chi ti dà l' IServiceoggetto.

Tuttavia, nel tuo esempio, Clientè l'unico che conosce i valori a cui vengono passati Initialize(). Se vuoi mantenerlo in questo modo, ti suggerirei quanto segue:

  • Aggiungi un IServiceFactorye passalo al Clientcostruttore. Quindi è possibile chiamare serviceFactory.createService(new Context(...))che ti dà un inizializzato IServiceche può essere utilizzato dal tuo client.

Le fabbriche possono essere molto semplici e consentono anche di evitare i metodi init () e utilizzare invece i costruttori:

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

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

Nel client OnStartup()c'è anche un metodo di inizializzazione (usa solo un nome diverso). Quindi, se possibile (se conosci i Contextdati), la factory dovrebbe essere chiamata direttamente nel Clientcostruttore. Se ciò non è possibile, è necessario archiviare IServiceFactorye chiamarlo OnStartup().

Quando le Servicedipendenze non sono fornite da Clientloro sarebbero fornite da DI attraverso 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);
    }
}

1
Grazie, proprio come pensavo, nell'ultimo punto ... E nel ServiceFactory, useresti il ​​costruttore DI nella fabbrica stessa per le dipendenze necessarie per il costruttore del servizio o il localizzatore del servizio sarebbe più adatto?
Dusan,

1
@Dusan non usa Service Locator. Se Serviceha dipendenze diverse da Context, che non sarebbero fornite da Client, possono essere fornite via DI ServiceFactoryal da passare a Servicequando createServiceviene chiamato.
Mr.Mindor

@Dusan Se è necessario fornire dipendenze diverse a servizi diversi (ovvero: questo ha bisogno di dipendenza1_1 ma il successivo ha bisogno di dipendenza1_2), ma se questo modello funziona diversamente per te, è possibile utilizzare un modello simile spesso chiamato modello Builder. Un Builder ti consente di impostare un oggetto frammentario nel tempo, se necessario. Quindi puoi farlo ... ServiceBuilder partial = new ServiceBuilder().dependency1(dependency1_1).dependency2(dependency2_1).dependency3(dependency3_1);ed essere lasciato con il tuo servizio parzialmente impostato, quindi in seguitoService s = partial.context(context).build()
Aaron

1

Il Initializemetodo deve essere rimosso IServicedall'interfaccia, poiché si tratta di un dettaglio di implementazione. Definire invece un'altra classe che prende l'istanza concreta di Service e chiama il metodo di inizializzazione su di essa. Quindi questa nuova classe implementa l'interfaccia IService:

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

        service.Initialize(context);
    }

    // Methods in the IService interface
}

Ciò mantiene il codice client ignaro della procedura di inizializzazione, tranne nel caso in cui la ContextDependentServiceclasse sia inizializzata. Limiti almeno le parti dell'applicazione che devono conoscere questa procedura di inizializzazione complessa.


1

Mi sembra che tu abbia due opzioni qui

  1. Spostare il codice di inizializzazione nel contesto e iniettare un contesto di inizializzazione

per esempio.

public InitialisedContext Initialise()
  1. Avere la prima chiamata a Esegui chiamata Inizializza se non è già stata eseguita

per esempio.

public async Task Execute()
{
     //lock context
     //check context is not initialised
     // init if required
     //execute code...
}
  1. Basta generare eccezioni se il contesto non è inizializzato quando si chiama Execute. Come SqlConnection.

L'iniezione di una factory va bene se vuoi solo evitare di passare il contesto come parametro. Supponiamo che solo questa particolare implementazione abbia bisogno di un contesto e non si desidera aggiungerlo all'interfaccia

Ma essenzialmente hai lo stesso problema, e se la fabbrica non avesse ancora un contesto inizializzato.


0

Non dovresti dipendere la tua interfaccia da alcun contesto db e metodo di inizializzazione. Puoi farlo nel costruttore di classi concrete.

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()
    {
        // ...
    }
}

E una risposta alla tua domanda principale sarebbe l' iniezione di proprietà .

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

        //...
    }

In questo modo è possibile chiamare tutte le dipendenze tramite Iniezione proprietà . Ma potrebbe essere un numero enorme. In tal caso, è possibile utilizzare Constructor Injection per loro, ma è possibile impostare il contesto in base alla proprietà controllando se è null.


OK, fantastico, ma ... ogni istanza del client deve avere la propria istanza del servizio inizializzata con dati di contesto diversi. I dati di contesto non sono statici o noti in anticipo, quindi non possono essere iniettati da DI nel costruttore. Quindi, come posso ottenere / creare un'istanza del servizio insieme ad altre dipendenze nei miei clienti?
Dusan,

hmm, quel costruttore statico non funzionerà prima di impostare il contesto? e inizializzare nelle eccezioni rischi costruttore
Ewan

Mi sto orientando verso l'iniezione di factory in grado di creare e inizializzare il servizio con i dati di contesto forniti (piuttosto che iniettare il servizio stesso), ma non sono sicuro che ci siano soluzioni migliori.
Dusan,

@Ewan Hai ragione. Proverò a trovare una soluzione per questo. Ma prima lo rimuoverò per ora.
Engineert,

0

Misko Hevery ha un post sul blog molto utile sul caso che hai affrontato. Entrambi avete bisogno di novità e iniettabili per la vostra Serviceclasse e questo post sul blog potrebbe esservi di aiuto.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.