Attributi alla riscossa
Nel precedente articolo sono state esaminate le basi per gestire l’Inversion of Control tramite plugin, in questo articolo si cercherà di dare più dignità alla classe PluginManager in modo da renderla un po’ più robusta. Il problema principale del codice del precedente esempio è che i plugin vengono individuati in base all’interfaccia implementata, generando di fatto due problemi: il primo è che l’esposizione di un plugin all’esterno della dll viene fatta in maniera implicita ed il secondo è che non si hanno informazioni dettagliate da mostrare all’utente. Per risolvere entrambi i problemi è necessario agire su due punti: dare la possibilità di decidere in maniera programmatica quali classi verranno esportate e fornire nel contempo un set di informazioni standard da mostrare all’utente o da utilizzare per gestire il plugin in maniera meno implicita.
Da questa mini analisi si evince subito che un attributo custom è probabilmente la soluzione ottimale dato che consente di aggiungere informazioni ad una classe sotto forma di metadati compilati nell’assembly e può essere utilizzato anche per marcare esplicitamente tutte le classi che dovranno essere gestite come plugin.
Creare un attributo custom per i plugin
Il primo passo è creare l’attributo per identificare i plugin come mostrato dalla classe PluginAttribute presente nell’esempio accluso. La creazione di un attributo custom esula da questo articolo, ma si può comunque notare che è sufficiente ereditare dalla classe Attribute e decorare la classe con l’attributo AttributeUsage che consente di specificare come deve essere gestito l’attributo stesso.
In questo caso si è specificato che il nostro attributo può essere utilizzato solamente con le classi e che non può essere ereditato. La classe attributo deve poi contenere le proprietà che identificano il plugin ed esporre un costruttore di convenienza per rendere più semplice l’utilizzo dell’attributo stesso. Nel caso in esame sono state scelte quattro proprietà distintive per il plugin: il nome, una sigla che indica il creatore, la versione ed infine una descrizione testuale che permette di specificare dettagliatamente cosa fa il plugin in questione.
Per completare la classe viene anche fatto l’override del metodo virtuale ToString() per dare una descrizione più user friendly dell’attributo.
Un istanza di un PluginAttribute è quindi a tutti gli effetti una descrizione completa di un plugin che può essere sia mostrata all’utilizzatore a titolo informativo, sia utilizzata internamente dal gestore per la catalogazione di tutti i plugin caricati.
Le altre classi di supporto
Prima di esaminare il codice del nuovo gestore è necessario creare altre classi ausiliarie necessarie per una gestione efficente. Prima di tutto si deve creare un altro attributo custom chiamato PluginInterfaceAttribute il cui scopo è specificare in maniera esplicita le interfacce implementate da una classe plugin.
In questo caso la proprietà AllowMultiple è pari a true dato che una singola classe può implementare se vuole più interfacce di plugin e quindi presentare un’istanza di questo attributo per ognuna di esse. Con questa tecnica abbiamo del tutto eliminato il caricamento implicito dato che ogni classe che vuole essere caricata dinamicamente deve essere decorata in maniera esplicita con una serie di attributi che ne identificano l’utilizzo.
L’ultima classe da esaminare si chiama PluginInfo ed il suo scopo è contenere tutte le informazioni su di un plugin caricato in modo da permetterne la gestione. Il costruttore accetta come parametro il tipo di dato da gestire ed internamente inserisce in una collection tutte le interfacce implementate recuperando tramite reflection.
Come si può notare le interfacce implementate vengono individuate semplicemente esaminando la la lista di attributi PluginInterface definiti sul tipo. La classe PluginInfo al suo interno tiene inoltre un riferimento al tipo di oggetto e fornisce un metodo ImplementInterface() che restituisce un boolean che indica se il tipo implementa una particolare interfaccia.
Il nuovo gestore di plugin
Il nuovo gestore internamente mantiene la lista di tutti i plugin caricati in un dictionary la cui chiave è un’istanza della classe PluginAttribute e il valore è un PluginInfo; due clausole using rendono più leggibile il codice dichiarando due alias di convenienza
Come per la versione precedente la classe PluginManager accetta nel costruttore il nome della cartella che internamente contiene i plugin da caricare e chiama la funzione AnalyzeAssemblyFile() su tutti i file .dll che trova nella cartella specificata.
Questa nuova versione è se possibile anche più semplice rispetto alla precedente, in particolare la scansione degli assembly è ora desicamente più lineare. Come nella versione precedente si esaminano tutti i tipi definiti nell’assembly e per sapere se il tipo esaminato deve essere caricato è sufficiente utilizzare il metodo IsDefined() della classe Type che permette di controllare se il tipo è stato decorato con un particolare attributo. Per identificare i plugin basta quindi cercare un attributo di tipo PluginAttribute e in caso positivo inserirlo nell’apposito dictionary creando il corrispondente PluginInfo().
Il gestore fornisce due soli metodi per interagire con l’esterno, GetPluginListForInterface() restituisce una lista di PluginAttribute contenente tutti i plugin che implementano una determinata interfaccia, mentre CreateInstance<T>() permette di creare un’istanza di plugin passando il corrispondente PluginAttribute.
Come si può vedere la creazione di nuove istanze viene fatta utilizzando l’oggetto Activator che chiama il costruttore di default dell’oggetto, come si può vedere l’implementazione è decisamente semplice e facile da gestire.
Cosa cambia lato client
Questi cambiamenti non complicano assolutamente il codice lato client. Il programma di esempio è costituito infatti dalla solita combobox con cui si seleziona il plugin da utilizzare e in aggiunta viene inserito un controllo PropertyGrid che permette di visualizzare tutti i dati dei plugin caricati.
Nell’evento FormLoad viene creata l’istanza del gestore passando la cartella contenente i plugin. Un client più efficace dovrebbe tenere una singola istanza del gestore con un pattern di tipo singleton, ma questo esercizio è lasciato al lettore. La creazione di un PluginManager è infatti piuttosto onerosa dato che internamente si utilizzano numerosi metodi di reflection.
Per visualizzare la lista dei plugin è sufficiente impostare il DataSource della ComboBox che automaticamente chiama il metodo ToString() per ogni oggetto della lista, mostrando di fatto la descrizione dell’attributo. Tramite la gestione dell’evento SelectedIndexChanged viene invece aggiornata la visualizzazione sulla PropertyGrid.
Infine la pressione del bottone crea un’istanza del plugin ed invoca il suo unico metodo Greet(). Anche in questo caso l’interfaccia generica consente di scrivere codice senza cast ed inoltre, dato che il metodo CreateInstance accetta un PluginAttribute, è sufficiente passare il parametro comboBox1.SelectedValue.
Come si può notare utilizzare il gestore è veramente immediato ed efficace e richiede poche linee di codice.
Implementare un plugin
L’ultimo cambiamento rispetto all’esempio precedente riguarda i plugin, che per poter essere caricati debbono ora essere compilati con gli appositi attributi, ecco ad esempio l’implementazione dell’EducatedGreeter.
Come si può vedere sono presenti due attributi, il primo imposta le informazioni base sul plugin ed il secondo indica l’interfaccia implementata.