SFRUTTARE LA DEPENDENCY INJECTION DI ASP.NET CORE
Iniziamo con un immagine che ci mostra il motore di un automobile, osserva quante piccole parti ci sono. Se gli ingegneri hanno deciso di fare tante piccole componenti è perché ognuna di esse ha un ciclo di vita diverso dalle altre. Prendiamo ad esempio le candele, hanno un ciclo di vita diverso rispetto al motore e i progettisti hanno deciso che tramite un’interfaccia rappresentata dalla filettatura, la candela non deve essere fusa nel motore ma facilmente sostituibile semplicemente svitando la vecchia e alloggiando la nuova. In questa sezione vedremo che anche ASP.NET Core è modulare, siccome i requisiti di un progetto cambiano, ci dobbiamo tenere pronti quando un componente software deve essere sostituito con un altro, tutto questo con il minimo sforzo.
Se gli ingegneri hanno scelto di suddividere il motore di un auto in tanti piccoli componenti è perché ognuno di essi ha un proprio ruolo all’interno dell’insieme e perché soprattutto deve poter essere sostituito indipendentemente dagli altri componenti. All’interno di un motore così come all’interno di un’applicazione ci sono componenti che hanno un ciclo di vita diverso dagli altri e devono poter essere sostituiti. Torniamo all’interno del CoursesController.
Il Controller e il CourseService non sono debolmente accoppiati, affatto, perché quando l’action Index o Detail va in esecuzione viene creata anche un’istanza di CourseService. È come se la candela di un’automobile non avesse la filettatura per essere smontata, ma fosse fusa all’interno del motore. Vediamo come va modificato il codice per rendere i due componenti meno accoppiati.
Come vedi nel Controller ho inserito un costruttore avente come parametro una istanza di CourseService. Tuttavia, se lanciamo l’applicazione otteniamo un errore in quanto l’istanza di CourseService ASP.NET Core non l’ha costruita. Per rimediare andiamo nel file startup e modifichiamolo come segue.
Con questa istruzione stiamo dicendo ai servizi di ASP.NET Core che devono prepararsi alla gestione di oggetti di tipo CourseService quando incontrano un componente come il nostro controller che ha una dipendenza da esso e che deve essere costruita e passata al costruttore del controller. Grazie alla dependency injection ASP.NET Core può creare istanze di componenti e iniettarle, in questo caso nel controller. Ora però il problema dell’accoppiamento non è del tutto risolto, ossia i due componenti si dice che sono fortemente accoppiati. Il CoursesController non può funzionare se non si fornisce una istanza di CourseService.
RENDERE I COMPONENTI DEBOLMENTE ACCOPPIATI
Se volessimo sostituire CourseService con un’altra classe sarebbe un problema perché dovremmo tornare di nuovo nel Controller e cambiare CourseService con il nome della nuova classe. Questo è un po’ come se il motore di un automobile potesse funzionare solo con le candele di uno specifico produttore. Quello che andiamo a fare ora è che CoursesController non dipenda più da CourseService ma da un’interfaccia, quando dovremmo sostituire il CourseService con qualcos’altro lo potremmo fare senza che il CoursesController ne sappia niente.
Torniamo all’interno del CoursesController e apportiamo la modifica, cioè sostituiamo un’implementazione concreta come CourseService con una interfaccia che non ha logica applicativa.
Ora sembra una banalità quello che abbiamo fatto, ma adesso in realtà il Controller dipende da una interfaccia che non ha, per sua definizione, alcun dettaglio implementativo. Quello che importa al Controller è che questo servizio esponga i metodi GetCourses() e GetCourse(id) non importa da dove provengono i dati, da un Web Services, da un database, etc. Ora dobbiamo andare nella classe Startup.cs e modificare la registrazione del servizio che non sarà più CourseService ma ICourseService. Se proviamo a lanciare il programma otteniamo un errore in quanto un’interfaccia non ha della logica applicativa e deve essere registrata con la sua concreta implementazione.
La dependency injection quando incontra nel Controller una dipendenza ad ICourseService in realtà costruisce un’istanza di CourseService. Ora con questo piccolo stratagemma possiamo cambiare CourseService con qualsiasi altra classe, l’importante è che implementi l’interfaccia.
SCEGLIERE IL CICLO DI VITA DI UN SERVIZIO
Quando creiamo un servizio non solo stiamo delegando ASP.NET Core alla creazione del servizio ma anche a gestirne il suo ciclo di vita. Le istanze create con la dependency injection a un certo punto dovranno pur essere distrutte per mantenere la memoria occupata su livelli accettabili.
Quando il Garbage Collector si rende conto che queste istanze non servono più perché inutilizzate le rimuove dalla memoria.
PROBLEMI DI RACE CONDITION
ASP.NET Core è una tecnologia Multithread, quando si utilizza AddSingleton bisogna fare attenzione ai problemi di Race Condition, questo perché può capitare che due Thread accedano contemporaneamente alla stessa risorsa.
Se due Thread arrivano contemporaneamente, supponendo che leggono il valore iniziale entrambi valorizzato nel campo privato a zero, entrambi lo incrementano di uno, ma questo è sbagliato perché sono arrivate due richieste.
Vediamo l’implementazione thread-safe del contatore. Ogni thread accede al campo privato uno alla volta, ha tutto il tempo di leggere il valore del contatore e incrementarlo. È un po’ come se ci fosse un moderatore.
RIEPILOGO DELLA SEZIONE
ACCOPPIAMENTO FORTE E DEBOLE DEI COMPONENTI
Quando costruiamo un’applicazione, dovremmo sempre fare in modo che i vari componenti siano sostituibili individualmente, un po’ come sostituire la candele di un’automobile non comporta la sostituzione del motore o della carrozzeria.
Questo è un accorgimento importante da tenere a mente perché i requisiti cambiano nel tempo e il nostro committente ci chiederà di modificare il comportamento dell’applicazione in alcuni punti. Perciò dobbiamo farci trovare pronti per acconsentire alla sua richiesta con il minimo dispendio di tempo ed energie.
Nella scorsa sezione abbiamo fatto interagire due componenti: il CoursesController e il CourseService. Rivediamo qual era il codice che avevamo usato all’interno dell’action Index.
- public IActionResult Index()
- {
- var courseService = new CourseService();
- List<CourseViewModel> courses = courseService.GetCourses();
- return View(courses);
- }
Questo codice presenta un problema perché rende il CoursesController fortemente accoppiato al CoursesService. Cioè, nel momento in cui vogliamo sostituire il CourseService con un’altra implementazione, che magari attinge i dati da un’altra fonte, dobbiamo tornare all’interno del CoursesController e sostituire a mano tutte le occorrenze di CourseService. In grandi applicazioni, questo può essere un lavoro dispendioso e propenso a errori. È come se le candele dell’automobile fossero inglobate all’interno del motore, costringendoci così a sostituire l’intero motore quando le candele si logorano dopo appena 50.000 km percorsi.
È per evitare questo problema che gli ingegneri meccanici hanno progettato sul motore un alloggiamento filettato facilmente accessibile dall’esterno, che rappresenta l’interfaccia di collegamento con la candela.
Una candela vecchia può essere facilmente sostituita con uno nuova semplicemente svitandola. Un semplice accorgimento che fa risparmiare molto tempo e denaro dato che la candela si usura molto prima del motore.
Anche noi, nella nostra applicazione, dovremmo fare in modo che i componenti siano facilmente sostituibili. Per questo, il linguaggio C# ci viene incontro con le interfacce.
Iniziamo definendo un’interfaccia che definisca i membri pubblici del servizio applicativo .
- public interface ICourseService
- {
- List<CourseViewModel> GetCourses();
- CourseDetailViewModel GetCourse(int id);
- }
Come si vede, l’interfaccia descrivere semplicemente quali sono i membri pubblici che una classe dovrà implementare e non il modo in cui deve implementarli. Il modo, invece, lo definirà la nostra classe CourseService che è un’implementazione concreta dell’interfaccia che abbiamo appena scritto.
Poi rechiamoci nel CoursesController e facciamo in modo che abbia una dipendenza da tale interfaccia. Le dipendenze le possiamo esprimere come parametri del costruttore.
Ora il nostro CoursesController è diventato debolmente accoppiato al servizio applicativo che fornisce i corsi perché non dipende più da un’implementazione concreta ma da un’interfaccia. Infatti, se vorremo sostituire CourseService con un’altra implementazione lo potremo fare senza toccare il controller.
Ci basta andare nel metodo ConfigureServices della classe Startup e indicare ad ASP.NET Core quale classe concreta deve costruire quando incontra componenti che dipendono dall’interfaccia ICourseService. Questo è necessario perché le interfacce non possono essere costruite.
Questa pratica è chiamata dependency injection, perché le dipendenze di un componente vengono passate dall’esterno (si dice anche “iniettate”). L’utilità di questa pratica risulterà ancor più evidente in futuro, quando affronteremo l’argomento del testing.
CICLO DI VITA DEI SERVIZI
ASP.NET Core, grazie al suo meccanismo di dependency injection, si preoccuperà al posto nostro di costruire le istanze di servizi come CourseService da cui i controller e gli altri componenti dipendono.
Ma… quand’è che ASP.NET Core distrugge le istanze ha creato, liberando così la memoria?
Dipende dal ciclo di vita che abbiamo indicato quando abbiamo registrato il servizio dal metodo ConfigureServices.
- Se usiamo services.AddTransient<ICourseService, CourseService>(), l’istanza potrà essere distrutta quando viene distrutto il componente che l’ha utilizzata, cioè quando il controller ha terminato la sua esecuzione;
- Se usiamo services.AddScoped<ICourseService, CourseService>(), l’istanza potrà essere distrutta solo al termine della richiesta HTTP corrente. Questo vuol dire che ASP.NET Core passerà la stessa identica istanza ad altri componenti che ne abbiano bisogno, almeno fintanto che la richiesta HTTP è ancora in corso. Come vedremo in seguito, questo può essere un ottimo modo per riutilizzare efficacemente servizi che sono “costosi” da costruire come il DbContext di Entity Framework Core che vedremo in seguito;
- Se usiamo services.AddSingleton<ICourseService, CourseService>(), l’istanza potrà essere distrutta solo quando l’applicazione viene arrestata. Questo vuol dire che ASP.NET Core creerà al massimo una sola istanza e la passerà a ogni componente che ne abbia bisogno, anche per richieste HTTP che arrivano da utenti diversi. Bisogna fare attenzione perché più thread contemporaneamente accederanno alla stessa istanza, causando severi effetti collaterali se non è stata progettata per essere thread-safe.
LINK AL CODICE SU GITHUB
Scaricare il codice della sezione09 o clonare il repository GITHUB per avere a disposizione tutte le sezioni nel tuo editor preferito.
Scrivi un commento