Inversion of Control
L' IoC è una tecnica di programmazione in cui le funzionalità di un software sono sviluppate in modo che invece di consistere in una serie di chiamate alle funzionalità del sistema, è il framework in cui sono usate a richiamare le funzionalità stesse.
Lo scopo dell'IoC è di migliorare la modularità e l'estendibilità, realizzando delle funzioni con un maggior grado di disaccoppiamento.
Sorge un problema su come e quando debbano essere istanziati gli oggetti che devono essere utilizzati nel sistema, visto che adesso è il framework che ne invoca i metodi e non viceversa.
A questo scopo sono state ideate diverse tecniche, di cui la principale è la dependency injection (D.I.).
Per alcuni versi l'IoC ha degli aspetti in comune con la D.I., negli obiettivi che si prefigge, ma la D.I. ha come focus principale l'eliminazione della costruzione diretta degli oggetti (service) negli oggetti o nelle funzioni (client), che viene demandata all'injector, mentre l'IoC ha più ha che fare col flusso di esecuzione del programma. Possiamo dire che la D.I. è una tecnica utile a realizzare l'IoC.
Una comune implementazione dell'IoC è la programmazione event driven, in cui il programma consiste di una serie di gestori di eventi collegati a vari oggetti, e dove il ciclo degli eventi dell'applicazione è gestito dal sistema e le funzioni del programma sono invocate a seguito di eventi scatenati nell'interfaccia (ad esempio). La programmazione ad oggetti, nella sua versione più pura, ossia oggetti che si scambiano messaggi mediante le proprie interfacce, ben si adatta ad una programmazione ad eventi, che appunto rappresenta un tipo di IoC.
L'IoC è spesso utilizzata quando un framework gestisce il flusso degli eventi e richiama specifici metodi di classi custom per rispondere agli eventi generati dall'utente nell'interazione con l'interfaccia a sua disposizione.
Dependency Injection
Per un maggiore disaccoppiamento, in linea con gli obiettivi della IoC, le classi dovrebbero essere create dinamicamente e senza richiedere nella loro implementazione degli specifici oggetti, utilizzando piuttosto delle interfacce e demandandone l'istanziazione ad un agente esterno (injector). In questo modo non solo si realizza un'indipendenza funzionale sull'invocazione delle funzioni di livello più alto, ma anche sulla dipendenza da specifiche implementazioni di classi, che vengono sostituite da interfacce negli utilizzatori.
Con la D.I. l'uso dei costruttori per generare nuove istanze è bandito nelle normali funzioni, ed è delegato di solito ad un agente esterno che sceglie quali classi concrete utilizzare in base a criteri stabiliti in file di configurazione o creati dinamicamente nell'inizializzazione del programma.
Un criterio diverso per evitare l'uso dei costruttori (le "new") sono le factory ed i service locator.
Simple Factory
La simple factory è una funzione in grado di fornire istanze di classi del tipo richiesto, di solito ragionando in termini di interfaccia esposta, e spesso è usata insieme a meccanismi di configurazione che influiscono sui tipi concreti che vengono restituiti.
Oppure i tipi restituiti possono essere dinamicamente cambiati con delle funzioni di "registrazione" di un certo tipo o interfaccia. Nella sua essenza una simple factory esporrà un metodo Register, per associare ad un interfaccia o ad un nome, un tipo di oggetto concreto, ed un metodo Create che creerà il tipo di oggetto concreto in base all'interfaccia specificata come parametro ed alle Register precedentemente eseguite.
Factory Method
Il factory method consiste nel delegare la creazione di oggetti a metodi di classi derivate. In questo modo la classe base sa che saranno costruiti oggetti che implementano una determinata interfaccia (o derivano da una classe base) ma non sa esattamente quali classi esattamente saranno create.
Service Locator
Il service locator è una funzione delegata a restituire oggetti creati altrove, ossia non necessariamente nuove istanze di oggetti ad ogni successiva chiamata. E' anche chiamato container perché contiene una collezione di classi, creandola da configurazione o dinamicamente. Il service locator nasconde i dettagli su come accedere ad istanze di una classe ed a quale classe concreta effettivamente accedere per usufruire di certo servizio (interface). La differenza tra un Service Locator ed una Factory è che il primo fornisce l'accesso a servizi (istanze di oggetti) esistenti, mentre la seconda serve a creare nuovi oggetti.
Le Factory ed i Service Locator sono considerate anti-pattern, perché necessitano comunque di un'invocazione di una funzione di create o di get da parte del client. In questo senso, la dependency injection è considerata superiore, ed è di solito implementata tramite la valorizzazione automatica, mediante meccanismi di reflection, dalla classe injector durante l'avvio dell'applicazione.
L'Inversion of control rappresenta l'applicazione di uno dei 5 principi fondamentali della programmazione Object Oriented, ossia i SOLID:
1. Single Responsibility Principle (SRP)
2. Open Closed Principle (OCP)
3. Liskov Substitution Principle (LSP)
4. Interface Segregation Principle (ISP)
5. Dependency Inversion Principle (DIP)
2. Open Closed Principle (OCP)
3. Liskov Substitution Principle (LSP)
4. Interface Segregation Principle (ISP)
5. Dependency Inversion Principle (DIP)
Il dependency inversion principle (DIP) , afferma che
A) moduli di alto livello non dovrebbero dipendere da moduli di basso livello, entrambi dovrebbero dipendere da astrazioni (interfacce)
B) Le astrazioni non dovrebbero dipendere dai dettagli implementativi, ed i dettagli dovrebbero a loro volta dipendere da astrazioni (interfacce)
Secondo questo principio, invece di avere una classe A che usa una classe B che usa una classe C,
è preferibile avere una classe A che usa l'interfaccia B, una classe B che usa l'interfaccia A e l'interfaccia C, e la classe C che usa l'interfaccia B. Le dipendenze dirette tra classi sono bandite.
Personalmente trovo molto comodo usare una combinazione del factory pattern e del service locator, creando un'interfaccia, molto rudimentale e che richiede poche righe di codice per l'implementazione, del tipo
/// <summary> /// Factory used to create registered instances of classes /// </summary> public interface IMetaFactory { /// <summary> /// Creates an concrete instance of an abstract class /// </summary> /// <typeparam name="T"></typeparam> /// <returns></returns> T createInstance<T>() where T : class; /// <summary> /// Register a concrete type attaching it to an abstract type /// </summary> /// <param name="concreteType"></param> /// <param name="abstractType"></param> void registerType(Type concreteType, Type abstractType); /// <summary> /// Creates a unique instance of type T /// </summary> /// <typeparam name="T"></typeparam> /// <returns></returns> T getSingleton<T>() where T : class; /// <summary> /// Register an instance of a class as the singleton for a specified type. /// </summary> /// <param name="T"></param> /// <param name="O"></param> void setSingleton(Type T, object O); }
E poi nell'inizializzazione del programma, o nella fase di bootstrap degli unit test, utilizzare i metodi registerType o setSingleton per impostare quali istanze di classi dovranno essere restituite dalla factory, che in questo caso funge anche da Service Locator. E' molto rudimentale ma in molti casi sufficiente. Certamente non ha lo stesso grado di disaccoppiamento che si può ottenere con la dependency injection, ma quando si lavora con codice legacy può risultare più facile introdurre una factory od un service locator, che è sempre meglio di usare direttamente delle "new" per creare gli oggetti.
Certamente è molto utile per creare degli unit test perché è possibile cambiare le istanze restituite dalla factory per restituire, invece, degli stub.
C'è da dire che in letteratura la definizione dell'IoC non è unanime, ad esempio è considerata da alcuni un principio di programmazione e da altri un design pattern. E, come tutti gli altri design pattern, non è detto che debba essere applicato ovunque in maniera ortodossa. E' tuttavia importante quando si vuole rendere dinamica l'invocazione di alcune funzioni ma si vuole consentire a run-time di cambiare i servizi utilizzati, ossia gli specifici oggetti (le classi concrete) su cui invocare i metodi.
Nessun commento:
Posta un commento