Panoramica sulla dependency injection

La dependency injection consiste nell'”iniettare” le dipendenze di un componente (classe, funzione o altro) anziché crearle al suo interno. Concretamente, si tratta di passare le dipendenze come parametri del costruttore della classe o del metodo/funzione che si sta invocando.

Si tratta di una delle tecniche più importanti per scrivere codice mantenibile, adattabile e testabile. Tra le altre cose, permette di rispettare alcuni dei principali principi dell’ingegneria del software, come “favour composition over inheritance” (“preferisci la composizione all’ereditarietà”) e l’open/closed principle (“un componente (classe o altro) dev’essere aperto alle estensioni ma chiuso alle modifiche”).

A sua volta, per poter iniettare le dipendenze in maniera efficace e adattabile, bisogna attenersi al principio “code against interfaces, not implementations”, ovvero, il codice che usa la dipendenza non dovrebbe basarsi su una specifica implementazione della stessa, ma sulla sua interfaccia. Si noti che in questo caso l’interfaccia è intesa in senso lato. Può essere effettivamente una interface, ma anche una classe astratta, o in generale un insieme di firme di metodi in comune tra più classi concrete (https://en.wikipedia.org/wiki/Duck_typing).

La dependency injection può essere effettuata “manualmente” oppure usando un container, che è un oggetto che viene configurato all’avvio dell’applicazione con le informazioni necessarie per istanziare le dipendenze (e.g. se un componente richiede una dipendenza di tipo ILogger, istanzia un oggetto MyLogger con certi parametri e passalo al componente; inoltre MyLogger dev’essere un singleton). Il secondo metodo è particolarmente utile quando si sta sviluppando l’applicazione con un framework (e.g.: .NET, Swing, ecc.), perché esistono librerie di dependency injection che si integrano con il framework e permettono di iniettare le dipendenze anche nei controller o in altri componenti non istanziati direttamente dal programmatore. Nelle nuove versioni, .NET ha già un container integrato, per cui non servono librerie aggiuntive.

Dependency injection e test automatici

Dal punto di vista dei test automatici, la dependency injection è necessaria per isolare il SUT (System Under Test), specialmente negli unit test. Ad esempio, permette di sostituire un ORM che si connette al database con un’implementazione che scrive e legge strutture dati in memoria contenenti dati opportunamente preparati dal tester.

Tipicamente, nel caso di unit test, le dipendenze del componente vengono sostituite con dei mockup (in senso lato; possono essere fake, stub, ecc.). I mockup implementano la stessa interfaccia dei componenti usati in produzione, ma sostituiscono l’implementazione reale o con un’operazione “vuota”, o con della logica che ritorna valori specifici a fronte di certi input (come nel caso del mockup dell’ORM) o che registra le chiamate al metodo in questione, in modo da poter successivamente effettuare delle asserzioni a riguardo.

Anche i mockup possono essere creati manualmente, scrivendo delle classi che implementano le interfacce in questione sostituendo l’implementazione, oppure usando delle librerie (e.g.: Moq, Mockito, ecc.), che sostanzialmente fanno la stessa cosa, tipicamente usando la reflection e/o il pattern decorator. Alcune librerie di dependency injection (e.g. autofac) hanno delle estensioni per integrarsi con alcuni framework di mockup, permettendo ad esempio di creare in automatico i mockup delle dipendenze di un componente. Tuttavia, solitamente è necessario avere un controllo più fine sul comportamento dei mockup, per cui, personalmente, preferisco non usare la dependency injection manuale nei test.

Conclusioni

La dependency injection è una tecnica che consente di modificare le dipendenze di un componente (e, di conseguenza, il suo comportamento) senza modificare il codice del componente stesso.

Oltre ad essere importante per scrivere codice mantenibile e adattabile, è fondamentale per isolare il SUT nei test automatici (in particolare negli unit test) ed è tra i prerequisiti per l’uso di altre tecniche e principi di ingegneria del software.

Per approfondimenti o consulenze in ambito testing e quality assurance, scrivere a info@dvisentin.com.