Abuso di Inversion Of Control, Il container questo sconosciuto… e così deve essere!!
L’astrazione ha tanti vantaggi ma porta anche tanti potenziali problemi soprattutto se la stiamo gestendo male o se stiamo astraendo troppo o addirittura dove proprio non serve. L’uso di un container per IoC tende a portare ad un uso esasperato dell’astrazione perchè si tende, all’inizio, ad astrarre anche il container stesso.
L’inghippo è che probabilmente non abbiamo ben presente la differenza tra Dependency Injection e Inversion of Control, perchè se ne parla sempre insieme ma sono due mondo molto diversi tra loro dove il primo può vivere di vita propria mentre il secondo nasce anche per risolvere problematiche introdotte dal primo.
Faccio prima a fare un esempio:
interface ISheafBuilderUn giornale quando va in stampa come prodotto finito ha dei “pacchi” di giornali che verranno consegnati, ad esempio alle edicole o agli abbonati, questi pacchi vengono creati seguendo un complesso dedalo di regole, regolette, regolucce e capricci (ad esempio le poste per le spedizioni massive hanno una quantità industriale di cavilli), una di queste regole recita più o meno:
{
IEnumerableBuildSheafs( IPublication publication, IEnumerable deliveries );
}
“… un pacco di giornali pronto per la consegna non deve pesare più di xx Kg…”Questo banalmente perchè il trasportatore non deve morire per il peso…
Nell’insieme la regoletta è abbastanza banale, il peso di un giornale è calcolabile sulla base nel numero di pagine e quindi possiamo variare dinamicamente la dimensione dei pacchi di volta in volta:
class SheafBuilder : ISheafBuilder… ma… se una volta realizzato il tutto qualcosa non va non sappiamo quale delle regole viene valutata “male/erroneamente” perchè tutto è all’interno di quel metodo BuildSheafs(), in soldoni non riusciamo a testare quello che succede dentro li. Sappiamo che fallisce ma non sappiamo perchè… troppe responsabilità.
{
public IEnumerableBuildSheafs( IPublication publication, IEnumerable deliveries )
{
//Bla… bla…
}
}
Single Responsability Principle
Facciamo fare ad ognuno il suo lavoro ed introduciamo un nuovo attore:
interface ISheafDimensionEvaluatorAbbiamo quindi un tizio che è in grado di dirci la dimensione massima di un pacco date il numero di pagine di una pubblicazione. Introduciamo una dipendenza e spieghiamo al mondo che il nostro SheafBuilder ha bisogno di questo nuovo signore per lavorare:
{
Int32 EvaluateMaxSheafDimension( Int32 pagesCount );
}
Dependency Injection
class SheafBuilder : ISheafBuilderQuesta è DI, nulla di più, iniettiamo le dipendenze. Il tutto funziona a prescindere dalla presenza di un container per IoC:
{
readonly ISheafDimensionEvaluator evaluator;
public SheafBuilder( ISheafDimensionEvaluator evaluator )
{
this.evaluator = evaluator;
}
public IEnumerableBuildSheafs( IPublication publication )
{
var maxDim = this.evaluator.EvaluateMaxSheafDimension( publication.PagesCount );
//Bla… bla…
}
}
var evaluator = new SheafDimensionEvaluator();Inversion Of Control
var builder = new SheafBuilder( evaluator );
var sheafs = builder.BuildSheafs( … );
Certo è che se estendiamo la soluzione proposta a tutto il nostro mondo la cosa si complica non poco…è evidente. Un service container ci viene allegramente in aiuto fornendoci un ottimo motore di risoluzione delle dipendenze, e molto altro:
var container = new WindsorContainer();la prima cosa che facciamo sarà quindi istruire il container su come è fatto il nostro mondo… e poi potremo allegramente, allegri oggi eh… ti spiego… week-end lungo in arrivo :-D, fare:
container.Register( Component.For()
.ImplementedBy() );
container.Register( Component.For()
.ImplementedBy() );
var builder = container.ResolveMolto meglio, decisamente, soprattutto se pensiamo su larga scala.();
var sheafs = builder.BuildSheafs( … );
Un motore di inversion of control è quindi qualcuno a cui possiamo chiedere qualcosa sapendo che se questo qualcosa ha delle dipendenze sarà onere ed onore del motore risolverle.Gli esempi su IoC che troviamo, in quanto esempi, sono sempre molto semplici e, spesso, si riducono a qualcosa del tipo:
var container = new WindsorContainer();Guardando gli esempi triviali e leggendo qua e la la prima cosa a cui si pensa è che il container in quanto tale è uno ed uno solo per ciclo di vita dell’applicazione (e questo è giustissimo) e da buoni pattern-addicted quali siamo l’equazione ci dice: singleton…e qui casca l’asino ;-)
var notifier = container.Resolve();
notifier.Notify( “Hello world!” );
Quando ci troviamo di fronte alla prima applicazione di un certo peso e decidiamo tutti orgogliosi che dobbiamo usare un container per IoC, memori della pensata di cui sopra, la prima cosa che facciamo è costruire una cosa del tipo:
static class DependencyContainerIDependencyContainer è un proxy generico verso un qualsiasi container sul mercato, nulla di trascendentale anzi. In questo modo possiamo facilmente fare:
{
public static IDependencyContainer GetContainer(){ /* singleton malefico ;-) */ }
}
public IEnumerableTutti tronfi guardiamo quello che abbiamo fatto e potremmo essere portati a pensare che è una vera figata:BuildSheafs( IPublication publication )
{
var maxDim = this.evaluator.EvaluateMaxSheafDimension( publication.PagesCount );
//Bla… bla…
var container = DependencyContainer.GetContainer();
var svc = container.Resolve<…>();
}
- abbiamo una classe statica che espone il nostro amato e inseparabile singleton ;-)
- recuperiamo dove ci serve il container e gli chiediamo di risolvere una dipendenza senza tante menate di costruttori e proprietà pubbliche;
- abbiamo astratto a sua volta anche il container, ci vogliono circa 30 minuti per farlo;
Ma se siete un framework guy non potete permettervi una dipendenza dal container perchè obblighereste chi usa il vostro prodotto a dipendere da quello specifico container. Come abbiamo visto la soluzione è decisamente semplice basta configurare correttamente le dipendenze dei singoli componenti, ma è sempre così facile?… abbiate pazienza, ci torniamo.
Perchè quello che abbiamo fatto nella realtà dei fatti è IL MALE? ;-)
Bhe… provate a testare quella roba… impossibile se non facendo i salti mortali per avere l’ambiente di test configurato correttamente, inoltre violate uno dei principi basilari dello Unit Testing: l’indipendenza dei singoli test, infatti la classe statica fa si che un secondo test si ritrovi il container già configurato… male, molto male.
Abbiamo però detto che non è sempre possibile risolvere ogni singola situazione con DI, ammetto che per ora a me è capitato solo 2 volte; vediamo un esempio:
Nella vostra fantastica applicazione basata su Composite UI siete in ascolto di un messaggio e, sulla base di alcuni parametri del messaggio in arrivo, dovete visualizzare una certa window/viewModel piuttosto che un altra, quindi dovete prendere questa decisione a runtime.La soluzione più semplice è iniettare il container stesso ;-), in fase di registrazione dei componenti potete fare:
In questo semplice esempio probabilmente avrete una classe preposta alla gestione del messaggio e non potete certo iniettare tutti i possibili viewModel, anche perchè potreste avere la necessità di visualizzare più volte la stessa UI contemporaneamente, ecco quindi che dovete avere la possibilità di accedere ad n istanze diverse direttamente a runtime.
var container = new WindsorContainer();e quindi far dipendere il vostro componente dal container:
container.Register( Component.For()
.Instance( container )
.ListStyle.Is( LisfeStyle.Singleton ) );
class MyComponentQuesto è molto testabile perchè nel test potete facilmente mockare quel IWindsorContainer. Ma a questo punto i puristi potrebbo obiettare che tutta la nostra applicazione dipende da quello specifico container e non hanno tutti i torti, sempre che abbia senso astrarre così tanto. Vediamo perchè:
{
public MyComponent( IWindsorContainer container )
{
//bla… bla…
}
}
- MyApplication.System
Contiene i contratti e non dipende da nessuno; - MyApplication.Runtime
Contiene l’implementazione dei contratti, non dovrebbe dipendere dal container pena l’impossibilità di rimpiazzare il container con facilità. Ma se dobbiamo iniettare il container stesso qui abbiamo una dipendenza. - MyApplication.Bootstrap
Dipende dal container, e va bene è l’entry point…
var container = new WindsorContainer();L’unica cosa che ci perdiamo è un po’ di sintassi cool con i generics. Ma non solo, purtroppo IServiceProvider è molto scarna come funzionalità quindi se avete bisogno di fare qualcosa di più della semplice Resolve/GetService siete nuovamente a piedi.
container.Register( Component.For()
.Instance( container )
.ListStyle.Is( LisfeStyle.Singleton ) );
class MyComponent
{
public MyComponent( IServiceProvider container )
{
//bla… bla…
var svc = container.GetService( typeof( IMyService ) ) as IMyService;
}
}
Inoltre dal punto di vista del design la soluzione di iniettare il conatiner introduce un potenziale problema derivante dal fatto che stiamo dando in mano al singolo componente la possibilità di fare tutto e controllare che faccia solo ciò che deve è un po’ complesso.
Factory, factory, factory
Ma non tutto è perso, anzi, possiamo fare molto meglio, nell’esempio di prima perchè non fare:
interface IMyServiceProviderIn questo modo otteniamo nuovamente:
{
T Resolve() where T : IMyServiceBase
}
class MyComponent
{
public MyComponent( IMyServiceProvider provider )
{
//bla… bla…
var svc = provider.Resolve();
}
}
- Dependency Injection, e quindi facilità di test;
- Sintassi figosa con i generics;
- possiamo esporre tutte le funzionalità necessarie;
- controllo su quello che MyComponent può fare;
Direi che ho finito… :-D, l’argomento è complesso e credo che solo “lo sbatterci il muso” possa farvi veramente capire quali sono i problemi e quali le soluzioni migliori e a quale prezzo.
Adesso come al solito non sparate sul pianista…
.m