Come supporto per l'istanza di chiamata per metodo senza fare riferimento al contenitore IoC al di fuori della composizione root?

6

Ho letto con interesse questo articolo che afferma:

A DI Container should only be referenced from the Composition Root. All other modules should have no reference to the container.

This means that all the application code relies solely on Constructor Injection (or other injection patterns), but is never composed. Only at the entry point of the application is the entire object graph finally composed.

Prendo per significare che tutte le istanze dovrebbero essere iniettate, mai istanziate da qualcosa di diverso dal contenitore IoC.

Sotto questo modello, questo non è permesso:

class MyClass {
    void SomeMethod() {
        var w = new Worker();
        w.DoSomething();
    }
}

Invece dovrebbe assomigliare più a questo:

class MyClass {
    protected readonly IWorker _worker;
    public MyClass(IWorker worker) {
        _worker = worker;
    }
    void SomeMethod() {
        _worker.DoSomething();
    }
}

Funziona bene se ho solo bisogno di un'istanza di Worker per istanza di MyClass . Ma cosa succede se Worker è stateful (o peggio, usa e getta) e ho bisogno di una nuova istanza ogni volta che si chiama SomeMethod ?

Potrei fare qualcosa di simile:

class MyClass {
    protected readonly IUnityContainer _container;
    public MyClass(IUnityContainer container) {
        _container = container;
    }
    void SomeMethod() {
        using (var w = _container.Resolve<IWorker>()) {
            w.DoSomething();
        }
    }
}

Ma questa tecnica è disapprovata da questo articolo , e inoltre viola il principio che il contenitore dovrebbe essere referenziato dalla radice della composizione.

Potrei iniettare una fabbrica:

class MyClass {
    protected readonly IWorkerFactory _factory;
    public MyClass(IWorkerFactor factory) {
        _factory = factory;
    }
    void SomeMethod() {
        using (var w = _factory.Create<IWorker>()) {
            w.DoSomething();
        }
    }
}

... che sembra affrontare il problema. Ma poi quando vado a scrivere la fabbrica, devo fare nuovamente riferimento al contenitore:

class WorkerFactory : IWorkerFactory
{
    protected readonly IUnityContainer _container;

    public WorkerFactory(IUnityContainer container) {
        _container = container;
    }

    public T Create<T>() where T : IWorker {
        return _container.Resolve<T>();
    }
}

... così davvero, a quanto pare, ho spostato il problema, non risolto.

Se è vero che "Un contenitore DI dovrebbe essere referenziato solo dalla radice di composizione", non vedo come sia possibile per l'istanza di chiamata per metodo.

Come posso supportare l'istanza di chiamata per metodo senza fare riferimento al contenitore al di fuori della composizione della radice?

    
posta John Wu 11.04.2017 - 01:01
fonte

3 risposte

6

La maggior parte dei contenitori IoC fornirà un metodo per l'implementazione automatica di una fabbrica. Vedi ad esempio:

Il contenitore IoC creerà un'istanza di una factory che, se configurata correttamente, restituirà un nuovo oggetto con ogni chiamata a un metodo.

Funziona, perché il contenitore genererà il codice che per la factory che fa riferimento al contenitore e lo utilizza per soddisfare le dipendenze degli oggetti che stai creando. Se il tuo contenitore IoC non è così maturo e non dispone di tale funzione, dovrai scrivere il tuo stabilimento e tale stabilimento dovrà probabilmente fare riferimento direttamente al contenitore IoC. Direi che questo caso è un'eccezione alla regola, e fintanto che i riferimenti al contenitore vengono tenuti sequestrati nelle implementazioni di fabbrica e non si estinguono da nessuna parte, seguirai comunque lo spirito della regola.

    
risposta data 11.04.2017 - 01:43
fonte
4

Innanzitutto, l'istanziazione per richiesta è spesso problematica per le app. Crea un sacco di spazzatura. Tende ad essere lento. Per applicazioni scalabili, è spesso evitato. Per la chiamata al metodo sarà anche peggio, soprattutto dal momento che la risoluzione IoC e l'istanziazione si rifletteranno e causeranno problemi.

In secondo luogo, una fabbrica con Create<T> non è in realtà una fabbrica, ma un ServiceLocator. Generalmente sono disapprovati mentre diventano oggetti di Dio.

Tutto ciò detto, invece di mettere un IWorker nel contenitore IoC, puoi invece mettere un Func<IWorker> (o un'interfaccia di strategia, o una fabbrica più tradizionale). Il consumatore può quindi creare un'istanza del lavoratore secondo necessità. Le sue dipendenze a loro volta dovrebbero essere statiche, rompendo la necessità di tornare ogni volta al contenitore. Invece, hanno passato le loro dipendenze quando hai impostato tutto per il contenitore.

E, francamente, se la fabbrica ha bisogno di un riferimento al contenitore per risolvere alcune dipendenze (nascondendo quella referenza nei dettagli di implementazione della fabbrica), quella non è la cosa peggiore al mondo. Le linee guida sono lì per aiutarti, non per ammanettarti.

    
risposta data 11.04.2017 - 01:50
fonte
2

Penso che il tuo ragionamento sia valido e includendo questo punto:

But then when I go to write the factory, I have to reference the container again:

class WorkerFactory : IWorkerFactory
{
    protected readonly IUnityContainer _container;

    public WorkerFactory(IUnityContainer container) {
        _container = container;
    }

    public T Create<T>() where T : IWorker {
        return _container.Resolve<T>();
    }
}

...so really, it seems, I have moved the problem, not solved it.

Non sono d'accordo sul fatto che non hai risolto il problema - considera alcuni dei problemi sollevati da Mark Seemann e come la tua soluzione si rivolge a quelli:

  • Il contenitore non è più una dipendenza di quelle classi di servizio che contengono la logica aziendale "reale".
  • Il WorkerFactory viene indirizzato in modo specifico a quelle classi che hanno effettivamente bisogno di creare un IWorker e sono fornite solo a quelle classi.
  • WorkerFactory aderisce a SRP - è responsabile solo della creazione di oggetti di durata limitata% co_de per le classi business / service.
  • I costruttori delle classi dominio / servizio non nascondono più le loro dipendenze dietro l'interfaccia IWorker .
  • La visibilità di metodi come IUnityContainer e RegisterInstance è ridotta al minimo. Le classi di servizio non sono più in grado di "sdrammatizzare" di soppiatto manomettendo il contenitore IoC.
  • Su una nota più soggettiva, lo scopo e l'utilizzo di RegisterType sembra più idiomatico per la creazione di oggetti (Principio di Least Astonishment).

Erik collegato ad esempi usando AutoFac, Windsor e Ninject che forniscono una soluzione pronta all'uso che fa lo stesso ed elimina IWorkerFactory arrotolata a mano, ma sembra realizzare lo stesso lavoro, in (presumibilmente) il allo stesso modo.

Per contenitori IoC come Unity che non hanno qualcosa di simile alla scatola (correggimi se ho torto), dovrebbe essere possibile scrivere una piccola estensione simile a WorkerFactory ancora più ampiamente riutilizzabile:

public class AbstractFactory<T> : IFactory<T>
{
    protected readonly IUnityContainer _container;

    public AbstractFactory(IUnityContainer container) {
        _container = container;
    }

    public T Create()  {
        return _container.Resolve<T>();
    }
}

Configurazione dell'unità:

  // I believe this is the correct syntax, but not tested!  
  // the concept should work with Unity anyway.
  container.RegisterType(typeof(IFactory<>), typeof(AbstractFactory<>));
  container.RegisterType(typeof(IWorker), typeof(Worker));

Il codice che deve creare istanze di breve durata sembra quasi lo stesso:

public class MyClass
{
    private readonly IFactory<IWorker> _workerFactory;

    public MyClass(IFactory<IWorker> workerFactory)
    {
        _workerFactory = workerFactory;
    }
}

Anche correlato: Mark Seemann ha dato un bel po 'di contributi su questo argomento a SO (una lista delle sue risposte qui: link ) Sembrano esserci diverse risposte in cui egli suggerisce semplicemente% vecchio% in una Fabbrica astratta.

Su un'altra nota correlata, il libro di Mark Iniezione delle dipendenze in .NET parla di dipendenze vissute (capitolo 6), e in particolare oggetti "limite" di cui è necessario gestire la durata e / o le risorse, come WorkerFactory , new o qualsiasi altra classe simile che funziona 'vicino al metal'.

Nel suo libro, Mark sostiene che quei tipi di classi non dovrebbero essere iniettati o creati con una fabbrica, e inoltre non dovrebbero essere esposti alle classi di servizio perché le loro interfacce sono orientate verso preoccupazioni tecniche e non preoccupazioni di business. Cioè una classe di business logic non dovrebbe mai gestire le sfumature di DbConnection con la sua stringa di connessione, errori di timeout, tentativi, ecc.

Il libro suggerisce di ignorare completamente IoC per questi tipi di oggetti "di confine" e di avvolgerli con astrazioni (astrazioni idealmente prive di status). Quelle astrazioni che nascondono la durata della vita / gestione delle risorse sarebbero responsabili di TcpClient 'ing (e DbConnection ' ing) l'oggetto di basso livello, mentre il contenitore IoC inietterebbe l'astrazione nella classe di servizio, che si preoccupa solo di problemi di alto livello come l'esecuzione di un comando o una query.

    
risposta data 11.04.2017 - 12:19
fonte