Libreria “friendly” di Dependency Inject (DI)


230

Sto riflettendo sulla progettazione di una libreria C #, che avrà diverse funzioni di alto livello. Naturalmente, queste funzioni di alto livello saranno implementate usando i principi di progettazione della classe SOLID il più possibile. Pertanto, ci saranno probabilmente classi destinate ai consumatori da utilizzare direttamente su base regolare e "classi di supporto" che sono dipendenze di quelle classi più comuni di "utenti finali".

La domanda è: qual è il modo migliore per progettare la libreria così è:

  • DI Agnostic - Anche se l'aggiunta del "supporto" di base per una o due delle librerie DI comuni (StructureMap, Ninject, ecc.) Sembra ragionevole, voglio che i consumatori siano in grado di utilizzare la libreria con qualsiasi framework DI.
  • Non utilizzabile da DI - Se un consumatore della libreria non utilizza DI, la libreria dovrebbe essere comunque il più facile da usare possibile, riducendo la quantità di lavoro che un utente deve fare per creare tutte queste dipendenze "non importanti" solo per arrivare a le classi "reali" che vogliono usare.

Il mio pensiero attuale è quello di fornire alcuni "moduli di registrazione DI" per le librerie DI comuni (ad esempio un registro StructureMap, un modulo Ninject) e un set o classi Factory che non sono DI e contengono l'accoppiamento a quelle poche fabbriche.

Pensieri?


Il nuovo riferimento all'articolo di link non funzionante (SOLID): butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
M.Hassan

Risposte:


360

Questo è in realtà semplice da fare una volta capito che DI riguarda schemi e principi , non la tecnologia.

Per progettare l'API in modo indipendente dal contenitore DI, seguire questi principi generali:

Programma per un'interfaccia, non un'implementazione

Questo principio è in realtà una citazione (dalla memoria) di Design Patterns , ma dovrebbe sempre essere il tuo vero obiettivo . DI è solo un mezzo per raggiungere questo scopo .

Applica il Principio di Hollywood

Il Principio di Hollywood in termini di DI dice: Non chiamare il contenitore DI, ti chiamerà .

Non chiedere mai direttamente una dipendenza chiamando un contenitore dal tuo codice. Chiedilo implicitamente usando l' iniezione del costruttore .

Usa iniezione costruttore

Quando hai bisogno di una dipendenza, richiedila staticamente tramite il costruttore:

public class Service : IService
{
    private readonly ISomeDependency dep;

    public Service(ISomeDependency dep)
    {
        if (dep == null)
        {
            throw new ArgumentNullException("dep");
        }

        this.dep = dep;
    }

    public ISomeDependency Dependency
    {
        get { return this.dep; }
    }
}

Nota come la classe di servizio garantisce i suoi invarianti. Una volta creata un'istanza, la dipendenza sarà garantita a causa della combinazione della clausola di guardia e della readonlyparola chiave.

Usa Abstract Factory se hai bisogno di un oggetto di breve durata

Le dipendenze iniettate con l'iniezione del costruttore tendono a durare a lungo, ma a volte è necessario un oggetto di breve durata o per costruire la dipendenza in base a un valore noto solo in fase di esecuzione.

Vedi questo per maggiori informazioni.

Componi solo all'ultimo momento responsabile

Mantieni gli oggetti disaccoppiati fino alla fine. Normalmente, puoi attendere e cablare tutto nel punto di ingresso dell'applicazione. Questa è chiamata radice della composizione .

Maggiori dettagli qui:

Semplifica usando una facciata

Se ritieni che l'API risultante diventi troppo complessa per gli utenti alle prime armi, puoi sempre fornire alcune classi di facciata che incapsulano le comuni combinazioni di dipendenze.

Per fornire una facciata flessibile con un alto grado di rilevabilità, potresti prendere in considerazione la possibilità di fornire fluenti costruttori. Qualcosa come questo:

public class MyFacade
{
    private IMyDependency dep;

    public MyFacade()
    {
        this.dep = new DefaultDependency();
    }

    public MyFacade WithDependency(IMyDependency dependency)
    {
        this.dep = dependency;
        return this;
    }

    public Foo CreateFoo()
    {
        return new Foo(this.dep);
    }
}

Ciò consentirebbe a un utente di creare un Foo predefinito scrivendo

var foo = new MyFacade().CreateFoo();

Tuttavia, sarebbe molto evidente che è possibile fornire una dipendenza personalizzata e si potrebbe scrivere

var foo = new MyFacade().WithDependency(new CustomDependency()).CreateFoo();

Se immagini che la classe MyFacade incapsuli molte dipendenze diverse, spero sia chiaro come fornire impostazioni predefinite corrette e allo stesso tempo rendere rilevabile l'estensibilità.


FWIW, molto tempo dopo aver scritto questa risposta, ho approfondito i concetti qui e ho scritto un post sul blog più lungo sulle biblioteche DI-Friendly e un post di accompagnamento su DI-Friendly Frameworks .


21
Anche se questo suona alla grande sulla carta, nella mia esperienza una volta che hai molti componenti interni che interagiscono in modi complessi, finisci con molte fabbriche da gestire, rendendo più difficile la manutenzione. Inoltre, le fabbriche dovranno gestire gli stili di vita dei loro componenti creati, una volta installata la libreria in un contenitore reale questo entrerà in conflitto con la gestione dello stile di vita del contenitore stesso. Le fabbriche e le facciate ostacolano i contenitori reali.
Mauricio Scheffer,

4
Devo ancora trovare un progetto che faccia tutto questo.
Mauricio Scheffer,

31
Bene, è così che sviluppiamo software in Safewhere, quindi non condividiamo la tua esperienza ...
Mark Seemann,

19
Penso che le facciate debbano essere codificate a mano perché rappresentano combinazioni conosciute (e presumibilmente comuni) di componenti. Un contenitore DI non dovrebbe essere necessario perché tutto può essere cablato a mano (si pensi al DI di Poor Man). Ricorda che una facciata è solo una classe di convenienza opzionale per gli utenti della tua API. Gli utenti esperti potrebbero comunque voler bypassare la facciata e collegare i componenti a proprio piacimento. Potrebbero voler usare il proprio DI Contaier per questo, quindi penso che sarebbe scortese forzare un particolare contenitore DI su di loro se non lo useranno. Possibile ma non consigliabile
Mark Seemann,

8
Questa potrebbe essere la migliore risposta che abbia mai visto su SO.
Nick Hodges,

40

Il termine "iniezione di dipendenza" non ha assolutamente nulla a che fare con un contenitore IoC, anche se si tende a vederli menzionati insieme. Significa semplicemente che invece di scrivere il tuo codice in questo modo:

public class Service
{
    public Service()
    {
    }

    public void DoSomething()
    {
        SqlConnection connection = new SqlConnection("some connection string");
        WindowsIdentity identity = WindowsIdentity.GetCurrent();
        // Do something with connection and identity variables
    }
}

Lo scrivi così:

public class Service
{
    public Service(IDbConnection connection, IIdentity identity)
    {
        this.Connection = connection;
        this.Identity = identity;
    }

    public void DoSomething()
    {
        // Do something with Connection and Identity properties
    }

    protected IDbConnection Connection { get; private set; }
    protected IIdentity Identity { get; private set; }
}

Cioè, fai due cose quando scrivi il tuo codice:

  1. Affidati alle interfacce anziché alle classi ogni volta che pensi che l'implementazione potrebbe dover essere modificata;

  2. Invece di creare istanze di queste interfacce all'interno di una classe, passale come argomenti del costruttore (in alternativa, potrebbero essere assegnate a proprietà pubbliche; il primo è l' iniezione del costruttore , il secondo è l' iniezione di proprietà ).

Niente di tutto ciò presuppone l'esistenza di una libreria DI, e in realtà non rende il codice più difficile da scrivere senza uno.

Se stai cercando un esempio di questo, non cercare oltre .NET Framework stesso:

  • List<T>implementa IList<T>. Se si progetta la propria classe da utilizzare IList<T>(o IEnumerable<T>), è possibile trarre vantaggio da concetti come il caricamento lento, come Linq to SQL, Linq to Entities e NHibernate fanno tutti dietro le quinte, di solito attraverso l'iniezione di proprietà. Alcune classi di framework accettano effettivamente un IList<T>argomento come costruttore, ad esempio BindingList<T>, utilizzato per diverse funzionalità di associazione dei dati.

  • Linq to SQL ed EF sono costruiti interamente attorno alle IDbConnectioninterfacce correlate, che possono essere trasmesse tramite i costruttori pubblici. Non è necessario usarli, però; i costruttori predefiniti funzionano bene con una stringa di connessione che si trova in un file di configurazione da qualche parte.

  • Se lavori mai su componenti WinForms hai a che fare con "servizi", come INameCreationServiceo IExtenderProviderService. Non è nemmeno davvero che cosa quali sono le classi concrete sono . .NET in realtà ha un proprio contenitore IoC IContainer, che viene utilizzato per questo, e la Componentclasse ha un GetServicemetodo che è l'effettivo localizzatore di servizi. Naturalmente, nulla ti impedisce di utilizzare una o tutte queste interfacce senza IContainerquel particolare localizzatore. I servizi stessi sono solo vagamente accoppiati al container.

  • I contratti in WCF sono costruiti interamente attorno alle interfacce. L'effettiva classe di servizio concreta viene di solito indicata per nome in un file di configurazione, che è essenzialmente DI. Molte persone non se ne rendono conto ma è del tutto possibile scambiare questo sistema di configurazione con un altro contenitore IoC. Forse ancora più interessante, i comportamenti di servizio sono tutti casi di IServiceBehaviorcui è possibile aggiungere in seguito. Ancora una volta, potresti facilmente collegarlo in un contenitore IoC e farlo scegliere i comportamenti rilevanti, ma la funzionalità è completamente utilizzabile senza uno.

E così via e così via. Troverai DI dappertutto in .NET, è solo che normalmente è fatto così perfettamente che non lo pensi nemmeno come DI.

Se si desidera progettare la libreria abilitata per DI per la massima usabilità, il suggerimento migliore è probabilmente quello di fornire la propria implementazione IoC predefinita utilizzando un contenitore leggero. IContainerè un'ottima scelta per questo perché fa parte del .NET Framework stesso.


2
La vera astrazione per un container è IServiceProvider, non IContainer.
Mauricio Scheffer,

2
@Mauricio: hai ragione ovviamente, ma prova a spiegare a qualcuno che non ha mai usato il sistema LWC perché in IContainerrealtà non è il contenitore in un paragrafo. ;)
Aaronaught il

Aaron, solo curioso di sapere perché usi un set privato; invece di specificare i campi come di sola lettura?
JR3

@Jreeter: Intendi perché non sono private readonlycampi? È fantastico se vengono utilizzati solo dalla classe dichiarante ma l'OP ha specificato che si trattava di un codice a livello di framework / libreria, il che implica una sottoclasse. In questi casi, in genere si desidera che dipendenze importanti siano esposte a sottoclassi. Avrei potuto scrivere un private readonlycampo e una proprietà con getter / setter espliciti, ma ... sprecare spazio in un esempio e più codice da mantenere in pratica, senza alcun vantaggio reale.
Aaronaught,

Grazie per aver chiarito! Supponevo che i campi di sola lettura fossero accessibili dal bambino.
jr3

5

EDIT 2015 : il tempo è passato, ora mi rendo conto che l'intera faccenda è stata un grosso errore. I contenitori IoC sono terribili e DI è un modo molto scarso di gestire gli effetti collaterali. In effetti, tutte le risposte qui (e la domanda stessa) devono essere evitate. Basta essere consapevoli degli effetti collaterali, separarli dal codice puro e tutto il resto cade in atto o è una complessità irrilevante e non necessaria.

Segue la risposta originale:


Ho dovuto affrontare questa stessa decisione durante lo sviluppo di SolrNet . Ho iniziato con l'obiettivo di essere DI-friendly e container-agnostico, ma quando ho aggiunto sempre più componenti interni, le fabbriche interne sono diventate rapidamente ingestibili e la libreria risultante era inflessibile.

Ho finito per scrivere il mio container IoC incorporato molto semplice, fornendo allo stesso tempo una struttura Windsor e un modulo Ninject . L'integrazione della libreria con altri contenitori è solo una questione di cablaggio corretto dei componenti, quindi potrei facilmente integrarlo con Autofac, Unity, StructureMap, qualunque cosa.

Il rovescio della medaglia è che ho perso la capacità di newaumentare il servizio. Ho anche preso una dipendenza da CommonServiceLocator che avrei potuto evitare (potrei rifattorizzare in futuro) per rendere più semplice l'implementazione del contenitore incorporato.

Maggiori dettagli in questo post sul blog .

MassTransit sembra fare affidamento su qualcosa di simile. Ha un'interfaccia IObjectBuilder che in realtà è IServiceLocator di CommonServiceLocator con un paio di metodi in più, quindi implementa questo per ogni contenitore, cioè NinjectObjectBuilder e un modulo / struttura normale, ovvero MassTransitModule . Quindi si affida a IObjectBuilder per creare un'istanza di ciò di cui ha bisogno. Questo è ovviamente un approccio valido, ma personalmente non mi piace molto dato che in realtà passa troppo nel container, usandolo come localizzatore di servizi.

MonoRail implementa anche un proprio contenitore , che implementa il buon vecchio IServiceProvider . Questo contenitore viene utilizzato in tutto questo framework attraverso un'interfaccia che espone servizi noti . Per ottenere il contenitore di cemento, ha un localizzatore di servizi integrato . La struttura Windsor punta questo localizzatore del fornitore di servizi su Windsor, rendendolo il fornitore di servizi selezionato.

In conclusione: non esiste una soluzione perfetta. Come per qualsiasi decisione di progettazione, questo problema richiede un equilibrio tra flessibilità, manutenibilità e convenienza.


12
Mentre rispetto la tua opinione, trovo le tue risposte troppo generalizzate "tutte le altre risposte qui sono superate e dovrebbero essere evitate" estremamente ingenue. Se da allora abbiamo imparato qualcosa, è che l'iniezione di dipendenza è una risposta alle limitazioni del linguaggio. Senza evolvere o abbandonare le nostre lingue attuali, sembra che avremo bisogno di DI per appiattire le nostre architetture e raggiungere la modularità. L'IoC come concetto generale è solo una conseguenza di questi obiettivi e sicuramente auspicabile. I contenitori IoC non sono assolutamente necessari per IoC o DI, e la loro utilità dipende dalle composizioni dei moduli che realizziamo.
TNE

@tne La tua menzione del fatto che io sia "estremamente ingenuo" è una pubblicità non gradita. Ho scritto applicazioni complesse senza DI o IoC in C #, VB.NET, Java (lingue che penseresti "necessitino" di IoC apparentemente a giudicare dalla tua descrizione), sicuramente non si tratta di limiti linguistici. Si tratta di limiti concettuali. Ti invito a conoscere la programmazione funzionale invece di ricorrere ad attacchi ad-hominem e argomenti mediocri.
Mauricio Scheffer

18
Ho detto di aver trovato la tua affermazione ingenua; questo non dice nulla di te personalmente ed è tutto sulla mia opinione. Per favore, non sentirti insultato da uno sconosciuto casuale. Insinuare che ignoro tutti gli approcci di programmazione funzionale pertinenti non è utile; non lo sai e ti sbaglieresti. Sapevo che eri ben informato lì ( ho dato una rapida occhiata al tuo blog), motivo per cui ho trovato fastidioso che un ragazzo apparentemente ben informato direbbe cose come "non abbiamo bisogno dell'IoC". IoC accade letteralmente ovunque nella programmazione funzionale (..)
tne

4
e in software di alto livello in generale. In effetti, i linguaggi funzionali (e molti altri stili) dimostrano in realtà che risolvono l'IoC senza DI, penso che entrambi siamo d'accordo con quello, ed è tutto ciò che volevo dire. Se sei riuscito senza DI nelle lingue che dici, come hai fatto? Presumo non usando ServiceLocator. Se si applicano tecniche funzionali, si finisce con l'equivalente di chiusure, che sono classi iniettate di dipendenza (dove le dipendenze sono le variabili chiuse). In quale altro modo? (Sono sinceramente curioso, perché neanche a me piace DI.)
tne

1
I contenitori IoC sono dizionari mutabili globali, che eliminano la parametricità come strumento di ragionamento, quindi da evitare. IoC (il concetto) come in "non chiamarci, ti chiameremo" si applica tecnicamente ogni volta che passi una funzione come valore. Invece di DI, modella il tuo dominio con ADT e poi scrivi interpreti (vedi Free).
Mauricio Scheffer,

1

Quello che vorrei fare è progettare la mia libreria in un modo agnostico contenitore DI per limitare il più possibile la dipendenza dal contenitore. Ciò consente di sostituire il contenitore DI con un altro, se necessario.

Quindi esporre il livello sopra la logica DI agli utenti della libreria in modo che possano utilizzare qualsiasi framework scelto attraverso l'interfaccia. In questo modo possono ancora utilizzare la funzionalità DI che hai esposto e sono liberi di utilizzare qualsiasi altro framework per i propri scopi.

Consentire agli utenti della libreria di collegare il proprio framework DI mi sembra un po 'sbagliato poiché aumenta notevolmente la quantità di manutenzione. Anche questo diventa quindi più un ambiente plug-in che un semplice DI.

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.