Negli ultimi anni si è molto studiato su come minimizzare le dipendenze tra gli oggetti, in modo da rendere le applicazioni molto più flessibili ed estendibili. Questo studio ha portato alla creazione di pattern particolari come quelli di Dependency Injection o Inversion of control, di cui il lettore può trovare abbondanza di informazioni semplicemente tramite google. In questo articolo si vuole invece esaminare una tecnica particolare che permette non solo di minimizzare la dipendenza tra gli oggetti, ma anche di estendere un’applicazione senza ricompilare.
Si consideri un semplicissimo esempio di un applicativo minimale che deve solamente salutare l’utente, sulla base del classico hello world. Il programma è costituito da una semplice classe chiamata standardGreeter ed una form che esegue un semplice saluto alla pressione di un bottone
Il codice è veramente banale, ma è già sufficiente per individuare una forte dipendenza con la classe StandardGreeter. Se ad esempio il nostro programma deve evolvere permettendo di eseguire un saluto più formale rispetto ad un semplice hello!! si potrebbe semplicemente realizzare un’ulteriore versione della classe chiamata FormalGreeter modificando contemporaneamente il codice dell’handler del bottone.
Questo approccio ha un enorme punto debole, il codice eseguito alla pressione del bottone deve conosce il tipo concreto di classe da istanziare per chiamare il suo metodo Greet() e quindi dipende dal tipo di oggetto utilizzato. Per risolvere elegantemente questo tipo di problematiche si può utilizzare un pattern di Abstract Factory, oppure utilizzare librerie come la Spring.NET per l’Inversion of control, o contenitori come il picocontainer, etc etc. In questo articolo la soluzione proposta è invece basata su plugin, una tecnica particolare per caricare dinamicamente il codice implementando così un pattern di Inversion of Control.
Caricamento dinamico di codice
Un plugin non è altro che un blocco di codice che viene caricato a runtime dal programma per estendere il funzionamento base. Il primo requisito per poter creare un plugin è definire la sua interfaccia, il programma host infatti non può caricare arbitrarie parti di codice se poi non sa come utilizzarle e dunque il primo passo per poter gestire un plugin è definirne l’interfaccia, proprio come se si stesse per realizzare una classica implementazione di Abstract Factory. Nell’esempio accluso è stata definita a questo proposito l’interfaccia IGreeter che comprende un’unica funzione e individua il contratto che deve essere implementato da un oggetto in grado di restituire un saluto.
La particolarità è che l’interfaccia viene definita in un progetto esterno a quello principale. La ragione è semplice dato che chi implementa un plugin deve avere un riferimento al progetto che contiene il contratto e quindi è più logico che tutte le interfacce che possono essere implementate da plugin esterni si trovino in un assembly dedicato che può essere così referenziato da chiunque voglia scrivere un plugin.
Nella solution di esempio si trovano infatti due implementazioni dell’interfaccia IGreeter, ognuna implementata in un differente assembly. Il progetto più interessante è comunque il PluginManager che contiene il codice per gestire il caricamento dinamico del codice. Dal punto di vista del programma principale un plugin è una classe che implementa un interfaccia riconosciuta e contenuto in un assembly esterno sconosciuto in fase di compilazione. Durante l’esecuzione è compito del PluginHandler scandire una particolare cartella in cui sono contenuti tutti i plugin al fine di identificare al suo interno tutti i tipi che implementano l’interfaccia richiesta ed occuparsi poi della creazione delle specifiche istanze. Il codice che effettua la scansione è veramente semplice
Come si può vedere al costruttore dell’oggetto PluginManager viene semplicemente passata la cartella da esaminare e il nome dell’interfaccia cercata. La funzione che effettua tutto il lavoro si chiama ScanDirectory() e itera semplicemente tra tutti i file con estensione dll presenti nella cartella invocando per ognuno di essi la funzione AnalyzeAssemblyFile(), la quale non fa altro che tentare di caricare l’assembly in memoria, iterare in tutti i tipi contenuti e cercare con la funzione GetInterface() i tipi che implementano l’interfaccia richiesta. Per ogni plugin trovato viene creato un nuovo oggetto PluginInfo che viene poi inserito in un dizionario indicizzato con il nome del tipo stesso. L’oggetto PluginInfo mantiene infatti al suo interno tutte le informazioni sul plugin caricato; internamente la classe PluginInfo ha due sole proprietà: il nome del plugin ed un riferimento al tipo di oggetto che lo implementa. Durante la creazione di un oggetto PluginInfo viene inoltre recuperato un riferimento al costruttore di default, utilizzato successivamente per creare nuove istanze.
Come si può notare il codice per gestire il plugin è veramente minimale. Il programma chiamante è anch’esso diverso dalla versione precedente, la form contiene infatti una combo in cui vengono listati tutti i plugin disponibili ed alla pressione del bottone viene utilizzato il greeter scelto dall’utente. Nel progetto incluso sono state inoltre aggiunte delle post build action ai due progetti in modo che, ad ogni compilazione, le dll con i plugin vengano inserite automaticamente nella sottocartella Plugins del programma principale.
Per capire il vantaggio di questa tecnica si cancelli il file EducatedGreeter.dll dalla cartella plugins e si esegua il programma; nella combo ora appare solamente lo StandardGreeter. A questo punto basta copiare nuovamente il file EducatedGreeter.dll nella cartella dei plugin, riavviare il programma e verificare che è nuovamente possibile utilizzare entrambi i greeter, senza dover ricompilare nulla. Se in futuro sorgerà la necessità di un ulteriore Greeter non si deve fare altro che implementare un nuovo plugin e copiarlo nella cartella apposita.
Conclusioni
L’utilizzo del caricamento dinamico di assembly e della reflection permette di implementare con veramente poche righe di codice una libreria che permette di gestire l’Inversion of Control con un ottica basata su caricamento dinamico di codice. Il programma finale è infatti in grado di utilizzare i nuovi plugin senza necessità di dover ricompilare l’applicativo o modificare file di configurazione, è sufficiente infatti copiare le nuove dll nella apposita cartella e “l’iniezione” del nuovo codice è completamente automatica.