ahahahrgh :-D
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 ISheafBuilder
{
IEnumerable<ISheaf> BuildSheafs( IPublication publication, IEnumerable<IDelivery> deliveries );
}
Un 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:
“… 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
{
public IEnumerable<ISheaf> BuildSheafs( IPublication publication, IEnumerable<IDelivery> deliveries )
{
//Bla… bla…
}
}
… 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à.
Single Responsability Principle
Facciamo fare ad ognuno il suo lavoro ed introduciamo un nuovo attore:
interface ISheafDimensionEvaluator
{
Int32 EvaluateMaxSheafDimension( Int32 pagesCount );
}
Abbiamo 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:
Dependency Injection
class SheafBuilder : ISheafBuilder
{
readonly ISheafDimensionEvaluator evaluator;
public SheafBuilder( ISheafDimensionEvaluator evaluator )
{
this.evaluator = evaluator;
}
public IEnumerable<ISheaf> BuildSheafs( IPublication publication )
{
var maxDim = this.evaluator.EvaluateMaxSheafDimension( publication.PagesCount );
//Bla… bla…
}
}
Questa è DI, nulla di più, iniettiamo le dipendenze. Il tutto funziona a prescindere dalla presenza di un container per IoC:
var evaluator = new SheafDimensionEvaluator();
var builder = new SheafBuilder( evaluator );
var sheafs = builder.BuildSheafs( … );
Inversion Of Control
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();
container.Register( Component.For<ISheafBuilder>()
.ImplementedBy<SheafBuilder>() );
container.Register( Component.For<ISheafDimensionEvaluator>()
.ImplementedBy<SheafDimensionEvaluator>() );
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:
var builder = container.Resolve<ISheafBuilder>();
var sheafs = builder.BuildSheafs( … );
Molto meglio, decisamente, soprattutto se pensiamo su larga scala.
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();
var notifier = container.Resolve<INotifier>();
notifier.Notify( “Hello world!” );
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 ;-)
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 DependencyContainer
{
public static IDependencyContainer GetContainer(){ /* singleton malefico ;-) */ }
}
IDependencyContainer è un proxy generico verso un qualsiasi container sul mercato, nulla di trascendentale anzi. In questo modo possiamo facilmente fare:
public IEnumerable<ISheaf> BuildSheafs( IPublication publication )
{
var maxDim = this.evaluator.EvaluateMaxSheafDimension( publication.PagesCount );
//Bla… bla…
var container = DependencyContainer.GetContainer();
var svc = container.Resolve<…>();
}
Tutti tronfi guardiamo quello che abbiamo fatto e potremmo essere portati a pensare che è una vera figata:
- 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;
Astrarre il container di per se è cosa inutile, sarebbe come dire voglio astrarmi dalla classe String… il container è infrastruttutura e come tale se serve deve essere noto dove serve. Inoltre se volete andare oltre le funzionalità di base, che già sono molte, c’è poco da fare ogni container, ad esempio, fa AOP/Policy Injection a modo suo e quindi dovete fare una scelta per la vita ;-)
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.
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.
La soluzione più semplice è iniettare il container stesso ;-), in fase di registrazione dei componenti potete fare:
var container = new WindsorContainer();
container.Register( Component.For<IWindsorContainer>()
.Instance( container )
.ListStyle.Is( LisfeStyle.Singleton ) );
e quindi far dipendere il vostro componente dal container:
class MyComponent
{
public MyComponent( IWindsorContainer container )
{
//bla… bla…
}
}
Questo è 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è:
- 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…
La soluzione più semplice è sotto i nostri occhi sin dal lontano 2001, nel framework infatti è ben noto da tempo il concetto di IoC: System.IServiceProvider, e i container blasonati (tra cui Castle Windsor) implementano quell’interfaccia… ergo:
var container = new WindsorContainer();
container.Register( Component.For<IServiceProvider>()
.Instance( container )
.ListStyle.Is( LisfeStyle.Singleton ) );
class MyComponent
{
public MyComponent( IServiceProvider container )
{
//bla… bla…
var svc = container.GetService( typeof( IMyService ) ) as IMyService;
}
}
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.
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 IMyServiceProvider
{
T Resolve<T>() where T : IMyServiceBase
}
class MyComponent
{
public MyComponent( IMyServiceProvider provider )
{
//bla… bla…
var svc = provider.Resolve<IMyService>();
}
}
In questo modo otteniamo nuovamente:
- 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;
Quello che ci manca adesso è semplicemente un’implementazione di IMyServiceProvider che dipenda dal container, per ovviare al problema della dipendenza, visto che usiamo IoC potremmo isolare questo provider in un assembly a se e quindi portarci a casa comunque la possibilità di rimpiazzare facilmente il container.
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