Magazine

System.AddIn: la nuova via dell’estensibilità secondo il Framework.NET v3.5

mauroservienti

08/04/2008

In seguito all'evento del 14 MArzo 2008, Mauro Servienti ha pubblicato questo articolo per spiegare ulteriormente le problematiche già espresse durante il meeting

0%100%
per esprimere un voto è necessario registrarsi al sito

.NET Framework, Architecture

introduzione

La visione odierna dell'architettura del software pone una particolare attenzione all'estensibilità, sempre più spesso si ha la necessità, per svariati motivi, di realizzare applicazioni che siano estensibili. L'estensibilità può rendersi necessaria per scopi diversi e può quindi essere vista da diversi punti di vista, che non sono necessariamente mutuamente esclusivi:

  • lo sviluppatore: pensiamo alla necessità di poter modificare a caldo alcune funzionalità dell'applicazione senza dover necessariamente ricompilare e redistribuire l'applicazione stessa; Un'architettura pensata per essere estensibile ci è di grandissimo aiuto in questa direzione;
  • il cliente: un'applicazione a "plugin" ci consente, e di conseguenza conseta al cliente, di sviluppare nuove funzionalità atte a soddisfare bisogni specifici; ci consente cioè di "verticalizzare" la nostra applicazione senza costringerci a legare a filo doppio l'applicazione con le necessità del cliente;
  • il mercato: se l'applicazione che sviluppiamo è la cosiddetta applicazione da banco, con diffusione su larga scala, l'estensibilità è sicuramente un punto a favore per tutti quelli che desiderano aggiungere funzionalità creando così un mercato parallelo fatto solo di sviluppo di plugin;

il contratto

Perchè un'applicazione basata su un modello a plugin possa funzionare è necessario che l'applicazione (host) e i plugin (add-in) condividano qualcosa, abbiano cioè un punto di contatto comune e noto ad entrambi che gli permetta di instaurare un dialogo certo e privo di possibili fraintendimenti. Questo punto di contatto è noto come contratto: generalmente un contratto viene definito, nel mondo del .NET Framework, come un'interfaccia, che isolata in un assembly a se stante e referenziata da entrambi (host e add-in), garantisce che il dialogo sia sempre basato su una lingua, il contratto appunto, noto ad entrambi.  

È possibile implementare il contratto sia come interfaccia che come classe astratta, non ci sono particolari controindicazioni nel seguire la strada della classe astratta che è particolarmente utile quando abbiamo bisogno di fornire delle funzionalità preconfezionate a chiunque si trovi ad implementare il nostro contratto. Vedremo nel seguito quali sono gli eventuali limiti dell'implementazione del contratto sottoforma di classe astratta piuttosto che di interfaccia.

Il contratto è tutto ciò che l'host conosce del suo add-in, se abbiamo la necessità che anche il add-in debba poter dialogare con l'host allora abbiamo bisogno di definire un'altro contratto che rappresenta la visione che il add-in ha dell'host.  

il problema

Un contratto per sua natura deve essere immutabile nel tempo, nonostante l'affermazione sia forte è presto spiegata: se definiamo una lingua comune tra due mondi (host e add-in), che non si conoscono se non atraverso il contratto, non possiamo permetterci in nessun modo di modificare tale contratto pena la perdita di capacità di dialoggo tra i due mondi. Questa perdita di capacità di dialogo a seguito della variazione del contratto è superabile solo con la modifica, ricompilazione e redistribuzione sia dell'host che dei add-in.

Nel mondo odierno, in cui l'evoluzione del software è rapidissima e le necessità del cliente cambiano repentinamente, questa limitazione è un'ostacolo apparentemente insormontabile.

Facciamo un esempio chiarificatore: supponiamo di avere un sistema di tracing basato su plugin, l'esempio è a puro scopo esemplificativo del problema e non ha nessuna velleità di essere un buon esempio di design, un possibile contratto potrebbe essere il seguente:  

public interface ILogger

    void LogInformation( String msg );
   
void LogWarning( String msg );
   
void LogError( String msg );
}

L'interfaccia ILogger definisce queli sono i metodi che il plugin dovrà implementare per suportare il sistema di tracing.  

È evidente sin da subito che un contrtratto di questo genere è estremamente limitante, ma supponiamo di aver realizzato un'applicazione che fa uso di questo modello e di averla distribuita a n clienti.

Con il passare del tempo ci rendiamo conto di due cose:

  1. come abbiamo già detto il modello è decisamente limitante;
  2. alcuni dei nostri clienti hanno sviluppato dei loro plugin, che aderiscono al nostro contratto, per effettuare il tracing verso una destinazione diversa da quella che avevamo inzialmente previsto.

In questa condizione se decidiamo di aggiornare il nostro modello, e di conseguenza il contratto, per portarlo verso un qualcosa di più flessibile, ci rendiamo subito conto che romperemmo la compatibilità della nostra applicazione con tutti i plugin esistenti. Supponiamo infatti di voler estendere il contratto verso una soluzione di questo tipo:

public interface ILogger

    void Log( String msgType, String msg );
}  

Questa versione del contratto è decisamente più flessibile perchè ci permette di fare tracing con una tipologia di messaggi decisamente più ricca ed estensibile ma purtroppo è decisamente incompatibile con la versione precedente.

il modello

Seguendo quello che abbiamo esposto sino ad ora possiamo rappresentare graficamente il nostro modello in questo modo:

Dipendenza statica

Il modello grafico evidenzia ancora di più come il legame tra host, add-in e contratto sia forte e a filo doppio.

Fortunatamente il .NET Framework 3.5, al momento della scrittura in Beta1, ci viene in aiuto introducendo un nuovo namespace: System.AddIn.

Il namespace System.AddIn introduce una serie di classi, suddivise tra interfacce, classi ed attributi, utili per la gestire tutti gli aspetti e le problematiche possibili che un'applicazione basata su un modello ad plugin può dover affrontare. Vediamo in primis come viene risolto, concettualmente, il primo dei problemi che abbiamo affrontato e che possiamo definire sinteticamente come versioning.

Il nuovo modello introdotto è sintetizzabile graficamente così:

 

Dipendenza statica

Come si vede abbiamo introdotto una quantità non indifferente di attori, vediamo nel dettaglio chi sono e a cosa servono:

  • Contratto: è sempre il contratto, nel nostro esempio ILogger, nel nuovo modello è un'interfaccia;
  • Host e Add-In: agli estremi del modello ci sono sempre l'applicazione (host) da un lato e le estensioni (add-in) dall'altro;
  • Viste: questa è la prima novità, come si nota dalle frecce delle dipendenze sia l'Add-In che l'Host hanno una sola dipendenza, che non è più dal contratto, come in precedenza, ma bensì da un Vista del contratto stesso.
  • Adattatori: anche questa è una novità, gli adattatori sono quei componenti che si precoccupano di adattare le chiamate che vanno dall'Host verso l'Add-In, ed eventualmente viceversa, al fine di, ove necessario, adattarle per risolvere i problemi di versioning. Dal punto di vista del codice il vero implementatore dell'interfaccia del contratto non è l'Add-In, o l'Host a seconda del punto di vista, ma bensì l'Adattatore stesso che fa da facade per l'elemento finale della catena.

Cerchiamo di chiarire meglio quest'ultimo punto perchè è il vero nodo cruciale, il processo di attivazione è graficamente descrivibile così:

 

Argomento del costruttore

Ereditarietà

Classi base astratte

Il risultato finale del processo di attivazione è molto semplice l'Host ha in mano un'istanza dell'Add-In sotto forma della sua "Vista v1 lato host", tutto ciò avviene secondo il seguente modello:

  • Viene creata un'istanza dell'Add-In v1;
  • L'Add-In v1 viene poi passato al costruttore dell'Adattatore v1 lato Add-In, sotto forma di AddInBase (che è la vista lato Add-In, e viene identificata programmaticamente con un attributo);
  • A questo punto l'Adattatore v1 lato Add-In viene passato, sotto forma di Contratto v1, al costruttore dell'Adattatore v1 lato Host;
  • Infine l'Adattatore v1 lato Host viene passato, sotto forma di Vista v1 lato Host, all'Host;

la retrocompatibilità

Nonostante il processo di attivazione sembri particolarmente oneroso in realtà la perdita di performance è veramente minima e ampiamente compensata dalla facilità con cui possiamo risolvere le problematiche di versioning; ritornando all'esempio del sistema di logging possiamo adesso risolvere brillantemente qualsivoglia problema di versioning nel seguente modo:

 

In questo caso vediamo come sia stato introdotto il nuovo contratto e un nuovo Adattatore lato Add-In che si preoccupa di gestire l'adattamento dal nuovo contratto verso quello vecchio. Questo adattatore è l'unica parte che deve essere riscritta per garantire la compatibilità degli Add-In che sia appoggiano su una vecchia versione del contratto. Al fine quindi di semplificare ulteriormente questo processo è fondamentale che ogni parte della catena si in un assembly separato che potrà essere sostituito a caldo consentendo un passaggio alla nuova versione ancora più indolore.

la gestione

Potremmo concludere la trattazione perchè ritengo che la carne che abbiamo messo al fuoco sia già più che sufficiente, ma le funzionalità offerte dal nuovo namespace non si fermano qui e meritano di essere almeno prese in considerazione.

discovery

Il processo di discovery, ovvero il processo di scoperta e analisi degli add-in disponibili, non è mai stato così semplice:

AddInStore.Update( addinPath );

Il metodo statico Update() della classe AddInStore si preoccupa di eseguire tutte le operazioni di discovery, noi non dobbaimo fare nulla di più.

activation

Il processo di attivazione è invece quella parte che si occupa di istanziare e attivare gli Add-In disponibili, anche in questo caso non dobbiamo fare operazioni particolarmente complesse:

IList<AddInToken> tokens = AddInStore.FindAddIns( typeof(AddInType), addinPath );

foreach( AddInToken token in tokens )
{
    token.Activate<AddInType>( AddInSecurityLevel.Internet );
}

Il metodo FindAddIns() ci consente di trovare tutti gli Add-In di un certo tipo e quindi ciclando sulla lista dei token che ci vengono restituiti possiamo attivare ogni singolo Add-In.  

isolation

Se osseriviamo con attenzione il metodo Activate() della classe AddInToken notiamo che al metodo viene passato un valore dell'enumerazione AddInSecurityLevel: questo overload del metodo Activate() ci permette di specificare il livello di sicurezza che deve essere assegnato all'Add-In che stiamo caricando, questa pratica nota come SandBoxing.

Perchè tutto ciò possa avvenire è necessario spiegare che gli Add-In in questo caso vengono caricati in un Application Domain diverso da quello dell'applicazione Host, creando un nuovo Application Domain possiamo decidere di assegnare una Evidence diversa a questo Application Domain e garantirci che gli'Add-In che girano in questo AppDomain non possano compiere azioni che non vogliamo concedergli.

Ma quest'ultima non è l'unica possibilità per isolare un Add-In, il metodo Activate() ha svariati overload che ci permettono di definire le modalità di isolamento da assegnare all'Add-In che stiamo attivando:

token.Activate<AddInType>( new AddInProcess(), AddInSecurityLevel.FullTrust );

Come vediamo è possibile attivare un Add-In addirittura in un processo separato, in questo modo mettiamo al sicuro la nostra applicazione anche da eventuali eccezioni non gestite all'interno dell'Add-In che causerebbero il blocco anche del processo della nostra applicazione. Naturalmente possiamo decidere di far condivire a più Add-In un solo processo:

AddInProcess  sharedProcess = new AddInProcess();

foreach (AddInToken token in tokens)
{
    token.Activate<AddInType>( sharedProcess ,     AddInSecurityLevel.FullTrust );
}

In questo caso tutti gli Add-In presenti nella lista tokens verrebbero attivati nello stesso processo (sharedProcess).

Il processo di isolamento porta con se però alcune limitazioni che dobbiamo tener ben presenti durante lo sviluppo: il fatto che un Add-In venga caricato in un Application Domain diverso da quello del chiamante o addirittura in un processo diverso comporta che perchè sia possibile superare il confine tra Host e Add-In l'unico modo sia comunicare attraverso remoting. La comunicazione attraverso remoting fa si che perchè un tipo (System.Type) sia veicolabile attraverso il confine è necessario che il tipo sia come minimo serializzabile e che tutti i tipi a cui fa riferimento siano a loro volta serializzabili, pone inoltre alcune limitazioni, comunque superabili, sull'eventuale gestione degli eventi che questi tipi complessi, che passano dall'Add-In verso l'Host e viceversa, espongono.

conclusioni

Alcune delle cose che abbiamo visto non sono nulla di nuovo, in particolare le modalità di isolamento degli Add-In, in molti casi si è scelto, per svariati motivi, di implementare l'estensibilità come abbiamo visto in queste pagine. La vera novità è la standardizzazione di questo meccanismo con tutti i benefici che questo comporta, inoltre il nuovo modello basato su Adattatori e Viste consente finalmente di poter gestire le problematiche di versioning in maniera semplice e funzionale.

Commenti
Nome

Sito web
Commento


indietro