Componenti in.net, Dependency Injection e Software testabile Giovanni Lagorio lagorio@disi.unige.it
Licenza Questi lucidi sono rilasciati sotto la licenza Creative Commons Attribuzione-Non commerciale-non opere derivate 3.0 Unported. Per leggere una copia della licenza visita il sito web http://creativecommons.org/licenses/by-nc-nd/3.0/ o spedisci una lettera a Creative Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA. In due parole, possono essere liberamente usati, copiati e distribuiti purché: 1. Venga citata la fonte originale 2. Non vengano usati in ambito commerciale 3. Non vengano modificati in nessun modo
Partiamo da un esempio: EmailSender public class EmailSender { } public bool SendEmail(string to, string body) { if (to == null) throw new ArgumentNullException("to"); if (body == null) throw new ArgumentNullException("body"); //... return false; } Questa classe e le sue classi/struct ausiliarie sono un buon esempio di componente (una volta impacchettate in una DLL)
BulkEmailSender public class BulkEmailSender { private readonly EmailSender _emailsender; private readonly string _footer; public BulkEmailSender(string footer) { this._emailsender = new EmailSender(); this._footer = footer; } } public void SendEmail(List<string> addresses, string body) { if (addresses == null) throw new ArgumentNullException("addresses"); if (body == null) throw new ArgumentNullException("body"); foreach (var a in addresses) { if (!this._emailsender.sendemail(a, body + this._footer)) throw new Exception("Cannot send email"); } }
Dipendenze Di cosa abbiamo bisogno per distribuire BulkEmailSender? Se EmailSender... non funziona/non ancora implementata? dipende da un DB? dall esistenza di una particolare cartella nel filesystem? Se il cliente volesse usare un altra componente per spedire le singole email? Come possiamo testarla? Se siamo offline? Se non vogliamo spammare durante il testing?
Classi come tipo Usare il nome di una classe come tipo crea un legame (troppo) forte Non si può: distribuire/riusare (facilmente) una classe se dipende da un altra sostituire un implementazione con un altra Come risolvere questi problemi? Lo sapete Usiamo le interfacce, ma certo! ma come si creano gli oggetti? Usiamo delle factory Sì, ma è molto meno ovvio di quello che sembra (la versione classica, costruttore privato, campo statico TheInstance NON va bene)
Non vogliamo/possiamo usare new Ovviamente, solo per quanto riguarda le classi che implementano le astrazioni/interfacce che ci interessano Va benissimo istanziare le collection, le classi di sistema e quelle appartenenti alla stessa componente Vari approcci Factory (tipicamente un Singleton, statico) Service Locator Dependency Injection
Situazione iniziale Bulk istanzia ES Assembly (DLL o EXE) Una componente è un unità indipendete di: produzione distribuzione/acquisizione In.NET questo vuol dire: componente=assembly
Un assembly, chiaramente, non basta usa/ dipende da IES implementa Bulk ES ES Possiamo astrarre introducento l interfaccia IES Nota: Bulk non ha bisogno di creare degli ES, ma gliene serve uno (rimuoveremo dopo questa semplificazione, che per Bulk è assolutamente ragionevole ma in altri scenari potrebbe non esserlo)
Ci siamo quasi usa/ dipende da IES implementa Bulk c d a ES x ES y z
Ma se Bulk dovesse creare degli IES? usa/dipende da IES IES Factory implementa Bulk ES istanzia ESF ES istanzia ES F Dimentichiamoci le altre classettine per semplicità
e consideriamo solo le dipendenze fra assembly IES IES Factory Bulk ES ESF ES ES F
Un esempio: il Testing Assert Unit Test SUT
Stub e Mock Assert Stub Unit Test SUT Mock Ok Se il costruttore istanzia gli oggetti o gli oggetti sono dei Singleton (statici)...come posso sostituirci degli stub/mock?
Cose ovvie Per testare un metodo (d istanza) la classe che lo contiene va istanziata Se il costruttore fa troppo può diventare difficile o troppo lento (connessione di rete, DB...) La presenza di inizializzatori/campi statici peggiora le cose Globali alla VM, non all applicazione Ogni unit-test deve, logicamente, istanziare una (porzione) di nuova app Devono fare/contenere cose diverse durante il testing L ordine di esecuzione diventa importante I test non possono essere lanciati in parallelo!
Classi (facilmente) testabili La responsabilità di creare degli oggetti va separata! Nei costruttori, invece di istanziare gli oggetti che servono, si richiedono tali oggetti Disinteressandosi sul come vengono istanziati si richiedono solo gli oggetti che servono direttamente (quelli che vengono copiati nei campi) Idealmente, il corpo di un costruttore dovrebbe essere una sequenza di assegnazioni Ricevere x e poi fare this.f = x.qualcosa... è molto sospetto
Principio di minima conoscenza (anche: Legge di Demetra) Supponete di dover pagare una birra. Al barista date: i soldi oppure il portafoglio, in modo che si prenda i soldi?
Equivalente in codice public void Purchase(Customer c) { Money m = c.getwallet().getmoney(); this.recordsale(, m); } Nel testing: Money m = new Money(5); Wallet w = new Wallet(m); Customer c = new Customer(w); Goods g = g.purchase(c); //...assert...
Classe Factory (singleton alla GoF ) E già qualcosa, Minimizza i cambiamenti (una sola new, invece di tante sparse qua e là) ma non risolve: Le classi dipendono dalla Factory invece che dalle implementazioni, ma lei dipende da loro... per transitività siamo al punto di partenza E statica (se non lo fosse, chi la istanzierebbe e come? Si sposterebbe solo il problema...) il singleton non static va benissimo ne parliamo dopo
Service Locator (Factory non statica?) Scarica la responsabilità di istanziare gli oggetti su un oggetto, il Service Locator (o Registry, Context, Manager, Environment,...), che si passa alle classi che devono istanziare interfacce Le vere dipendenze sono nascoste dal fatto che ogni classe dipende dal Service Locator Possibile, ma difficile il testing: cosa ridefinisco/modifico nel Service Locator per creare gli stub? Ogni classe dipende dal Service Locator, che sa istanziare tutte le classi, quindi ogni classe dipende da tutte le altre...
Dependency Injection Esistono vari tipi di DI, consideriamo la constructor injection (è quella da usare di default ) che inietta tramite i costruttori Altre sono la method, property, field,... L idea è estremamente semplice: i costruttori richiedono gli oggetti che servono direttamente, invece di crearli
Nel nostro esempio: public BulkEmailSender(string footer) { this._emailsender = new EmailSender(); this._footer = footer; } public BulkEmailSender(IEmailSender emailsender, string footer) { } this._emailsender = emailsender; this._footer = footer;
Il testing diventa facile! [TestFixture] public class TestBulkEmailSender { [Test] public void TestSendMailEmailSenderSucceeds() { var bulk = new BulkEmailSender(mock_ok, "bla bla"); bulk.sendemail(new List<string> { "a@a.com", "b@b.com" }, "hi!"); } } [Test] [ExpectedException(typeof(Exception))] public void TestSendMailEmailSenderFails() { var bulk = new BulkEmailSender(stub_fails, "bla bla"); bulk.sendemail(new List<string> { "a@a.com", "b@b.com" }, "hi!"); }
Nei test è ragionevole Notate che: Usare new: non c è nulla di male nel fatto che il codice di test dipenda da ciò che sta testando Testare classi non pubbliche (i dettagli di come vedere cose internal li vedremo quando parleremo di Unit Testing) Per TAP i test saranno gli stessi per tutte le diverse implementazioni, quindi dovrete istanziare interfacce (come spiegheremo nel seguito) Il fatto che il codice sia più facilmente testabile è solo uno dei vantaggi della DI, non l unico motivo di usarla
Dubbi (classici) Ma se ho 10 campi il costruttore deve richiedere 10 oggetti? Non sono troppi? Salvo eccezioni, sì deve richiedere 10 parametri Eccezioni: le classi valore (o le collection), ovvero le foglie del grafo degli oggetti, si possono istanziare senza problemi (poiché non hanno dipendenze a loro volta) Se i parametri sono troppi, è probabile che la classe abbia troppe responsabilità, ma ce le aveva già prima di usare la DI! La DI rende esplicite le dipendenze, non le diminuisce e non le aumenta Quindi, se una classe di basso livello ha bisogno, diciamo, di un logger, lo devo passare lungo tutta la catena? NO! Tipico fraintendimento... vediamo un esempio
Tradizionalmente...
Con la DI...
In codice... (versione tradizionale con singleton) DB.Init("..."); // inizializza il singleton Logger.Init(); // idem, l'ordine e` importante! var bulk = new BulkEmailSender(); // apparentemente indipendente da sopra // ma se il Logger non e` stato inizializzato // EmailSender (istanziato da BulkEmailSender) // si schianta
Con la DI... var db = new DB(...); var logger = new Logger(db); var emailsender = new EmailSender(logger); var bulk = new BulkEmailSender(emailSender, ""); // sbagliando l'ordine, la compilazione // fallisce! :-)
DI container Siccome le dipendenze sono esplicite, la composizione degli oggetti a volte si può automatizzare E quello che fanno i container DI Si programmano associando implementazioni a interfacce: Via codice, max flessibilità e controllo statico sui tipi, ma la riconfigurazione richiede ricompilazione Via file di configurazione (tipicamente XML) E poi loro gestiscono la creazione e lo scope (singleton, factory o container)
Configurazione Autofac (via codice) var builder = new ContainerBuilder(); builder.register<emailsender>().as<iemailsender>(); builder.register<bulkemailsender>(); // sto barando un pochino (assumo che l'unico // argomento sia l'iemailsender) IContainer container = builder.build(); var bulk = container.resolve<bulkemailsender>(); bulk.sendemail(new List<string>() {"a@a.com"}, "");
Senza barare... ( ma usando le che dobbiamo ancora vedere a lezione) var builder = new ContainerBuilder(); builder.register<emailsender>().as<iemailsender>(); builder.register(c => new BulkEmailSender( c.resolve<iemailsender>(), "ciao" ) );
Scope degli oggetti Singleton (è il default): viene creato un solo oggetto Si abilita con SingletonScoped() sul builder Factory: a ogni richiesta viene creato un nuovo oggetto Container: viene creato un oggetto per container I container possono essere innestati, viene gestita la distruzione deterministica via IDisposable Contextual: simile a container, viene creato un oggetto per contesto
Configurazione via XML builder.registermodule(new ConfigurationSettingsReader("autofac", "pippo.xml")); <?xml version="1.0" encoding="utf-8"?> <configuration> <configsections> <section name="autofac" type="autofac.configuration.sectionhandler, Autofac"/> </configsections> <autofac> <components> <component type="emailer.emailsender, Emailer" service="emailerinterfaces.iemailsender, EmailerInterfaces" /> <component type="emailer.bulkemailsender, Emailer" scope="factory"> <parameters> <parameter name="footer" value="ciao" /> </parameters> </component> </components> </autofac> </configuration>
Altro dubbio classico Ma si devono per forza creare tutti gli oggetti all inizio? Subito dopo la prima chiamata a Resolve? No! All inizio vengono creati solo gli oggetti necessari a far partire il sistema Se una classe ha bisogno di creare istanze (di intefacce) riceverà nel costruttore un oggetto Factory (di tipo interfaccia) per crearle NON si passa il container in giro (altrimenti sarebbe un Service Locator) Vediamo un esempio...
Esempio di (non static) Factory public class C { private readonly IPointFactory _pointfactory; private readonly ILineFactory _linefactory; public C(IPointFactory pointfactory, ILineFactory linefactory) { this._pointfactory = pointfactory; this._linefactory = linefactory; } public void DoSomething(/*... */) { //... ILine newline = CreateLine(/*... */); //... } private ILine CreateLine(int x0, int y0, int x1, int y1) { IPoint p0 = this._pointfactory.create(x0, y0); IPoint p1 = this._pointfactory.create(x1, y1); return this._linefactory.create(p0, p1); } }
Terminologia Va di moda parlare di: Inversion of Control e Dependency Injection e non mancano: le implementazioni di IoC/DI container Autofac, Castle of Windsor, Guice, NanoContainer, Ninject, PicoContainer, Spring, Spring.NET, Unity, Winter4net la confusione: IoC e DI sono la stessa cosa?
IoC e DI In questi contesti, sì; sono la stessa cosa (IoC container = DI container), ma IoC è un concetto più generale Ogni framework usa IoC (quando associate un azione a un pulsante in una GUI lo state usando) DI, pur apparentemente fuorviante, è il nome più corretto sebbene Autofac si autodefinisca a fresh approach to IoC (uff!)
Approfondimenti/crediti The clean code talks... (ce ne sono diversi) by Miško Hevery http://www.youtube.com/watch?v=rlflcwkxhj0 Inversion of Control Containers and the Dependency Injection pattern by Martin Fowler http://martinfowler.com/articles/injection.html