Archietture a plugIn, pensieri e considerazioni

Archietture a plugIn, pensieri e considerazioni

Uno dei modelli architetturali che più mi piace adottare per garantirmi sistemi aperti e personlaizzabili senza toccare il core applicativo è l'architettura a plugIn. In parole povere il goal di questo tipo di artchitettura sta nel creare modelli di comportamento che poi vengono implementati da librerie esterne caricate a runtime. Un esempio di applicazione di tale architettura la vedo bene nell'implentazione dei DataProvider del DAL. Mi piace infatti definire un DAL astratto fatto di modelli comportamentali (classi base astratte e/o interfacce).

Ecco due righe di esempio pratico per capire cosa intendo. L'esempio illustra un architettura molto semplice mirata a far capire il concetto del plugIn. Ho per questo volutamente omesso complicazioni legate ai pattern di creazione dei provider concreti e i dettagli implementativi degli metodi specifici.

public interface IPersonsDataProvider
{
    void Inizialize(string configurationString);
    PersonsCollection Retrieve();      
}
public /* static */ class PersonsDataProviderFactory
{
    public static IPersonsDataProvider GetPersonsDataProvider()
    {
        string providerTypeName = ConfigurationSettings.AppSettings["personsProviderTypeName"];
        string providerConfiguartionString = ConfigurationSettings.AppSettings["personsProviderConfiguartionString"];
        
        IPersonsDataProvider p = (IPersonsDataProvider)Activator.CreateInstance(Type.GetType(providerTypeName));
        p.Inizialize(providerConfiguartionString);
        return p;
    }
}
public /* static */ class PersonsManager
{
    public static PersonsCollection Retrieve()
    {
        IPersonsDataProvider provider = PersonsDataProviderFactory.GetPersonsDataProvider();
        return provider.Retrieve();        
    }
}

...implementiamo ora un paio provider concreti...

public class SqlClientPersonsDataProvider
{
    private string connectionString;
    
    public void Inizialize(string configurationString)
    {
        this.connectionString = configurationString;
    }    
    public PersonsCollection Retrieve()
    {
        PersonsCollection persons = new PersonsCollection();
        SqlConnection conn = new SqlConnection(this.connectionString);
        
        //TODO: Apertura della connessione, esecuzione del comando e mappatura dei dati.
        
        return persons;
    }
}
public class FileSysPersonsDataProvider
{
    private string fileName;
    
    public void Inizialize(string configurationString)
    {
        this.fileName = configurationString;
    }    
    public PersonsCollection Retrieve()
    {
        PersonsCollection persons = new PersonsCollection();
                
        //TODO: Apertura del file, lettura riga per riga e mappatura dei dati.
        
        return persons;
    }
}

Nell'esempio che ho qui sopra riportato PersonsManager sa che chiedendo al PersonsDataProvider potrà avere una lista di persone ma ignora dove tale elenco è stato letto: è quindi noto il comportamento del PlugIn ma si ignora l'implementazione.

Bene, ora che ci siamo chiariti quello che intendo per architettura a PlugIn posso introdurre le riflessioni di cui voglio parlare. Questo Week end ho fatto un paio di riflessioni: meglio avere un interfaccia che i plug devono implentare (come nel mio esempio) o una classe base che devono estendere? La questione credo che sia interessante perchè ognuno in merito ha i suoi pro e i suoi contro. Ho quindi provato a fare un elenco di pro e contro cercando di individuare dei workaround per i problemi tecnici che si potrebbero incontrare.

Molti preferiscono usare la classe base in modo che il plug abbia facilmente a disposizione alcune funzionalità base del sistema. Questa idea preferisco scartarla. Se ho esigenza di dare al plugin visibilità ad alcune funzionalità base del sistema preferisco implentare una classe Helper che sia di facciata al sistema. Ricordiamoci che abbiamo a disposizione una sola classe base da cui estendere per cui potrebbe essere un grosso limite per chi deve implementare il plugin.

Credo che la diatriba classe base da estendere o interfaccia da implementare sia più interessante - e meno filosofica - quando siamo in un contesto di upgrade del sistema con esigenza di aggiungere/modificare funzionalità al plugIn e non si ha pieno e/o facile controllo sulla modifica dei plugIn già implementati. Se le funzionalità da aggiungere sono bloccanti per il sistema ovviamente occorre - ahimè - rimettere mano a tutti plugIn... ma se le funzionalità da aggiungere non sono bloccanti si potrebbe rischiare se non si sta attenti di buttare nel cestino tutti i plugIn già implementati per il nostro sistema. Vediamo di capire cosa intendo con un esempio.

In seguito alla richiesta di recupero di una persona dato il suo codice faccio la modifica qui sotto riportata all'interfaccia del provider.

public interface IPersonsDataProvider
{
    void Inizialize(string configurationString);
    PersonsCollection Retrieve();      
    Person RetrieveByCode(string Code); 
}

La semplice modifica che ho fatto farà si che tutti i plugin già realizzati vengono a perdere di validità e il loro caricamento a runtime andrà in errore. Definire un interfaccia implica la definizione di una constraint forte. Se avessimo usato un classe astratta le cose sarebbe andate meglio? Beh qualche piccola semplificazione ci sarebbe. Ecco un esempio di come potrebbe essere definita una classe astratta base per il nostro DataProvider.

public class abstract PersonsDataProviderBase
{
    public abstract void Inizialize(string configurationString);
    public abstract void PersonsCollection Retrieve();      
}

Poi in seguito alla richiesta di modifica avremmo potuto fare la modifica come segue.

public class abstract PersonsDataProviderBase
{
    public abstract void Inizialize(string configurationString);
    public abstract void PersonsCollection Retrieve();      
    public virtual Person RetrieveByCode(string Code) 
    {
        PersonsCollection persons = Retrieve();
        foreach(Person person in persons)
        {
            if(person.Code == code)
            {
                return person;
            }            
        }
        return new UnknownPerson();
    }
}

Così facendo i plugin fino ad ora implementati continuerebbero a funzionare garantendo tempi più rilassati per apportare le dovute modifiche... ma non avremmo la garanzia che i nuovi DataProvider implementino correttamente tutti i metodi, qualche programmatore un pò svogliato implementerebbe solo i metodi astratti senza sentirsi in dovere di implementare tutti i metodi. ;-p

Quale portebbe essere la soluzione al caso dell'uso delle interfacce? Un workaround potrebbe essere quello che segue.

public interface IPersonsDataProviderV2: IPersonsDataProvider
{
    Person RetrieveByCode(string Code);
}
// ...
public /* static */ class PersonsManager
{
    public static PersonsCollection Retrieve()
    {
        IPersonsDataProvider provider = PersonsDataProviderFactory.GetPersonsDataProvider();
        return provider.Retrieve();        
    }
    public static Person RetrieveByCode(string Code) 
    {
        IPersonsDataProvider provider = PersonsDataProviderFactory.GetPersonsDataProvider();
        if(provider is IPersonsDataProviderV2)
        {
            ((IPersonsDataProviderV2)provider).RetrieveByCode(string code)            
        }
        else
        {        
            PersonsCollection persons = Retrieve();
            foreach(Person person in persons)
            {
                if(person.Code == code)
                {
                    return person;
                }            
            }
            return new UnknownPerson();
        }    
    }
}

Per il nostro esempio abbiamo avuto fortuna... per il metodo richiesto era facilmente implementabile una soluzione alternativa. Se non è possibile? In tal caso si potrebbe scegliere di far tornare una collezione vuota, un elemento speciale o sollevare una NotSupportedException o una NotImplementedException (rimando per la filosofia delle eccezioni a "The difference between NotSupportedException and NotImplementedException".)

Che conclusioni mi sono fatto in merito al problema? Storicamente sono sempre stato legato all'uso di interfaccia - ma non ho mai affrontato il problema che ho qui trattato. Scrivendo il post mi sono invece ricreduto... L'uso di interfacce di upgrade potrebbe facilitare l'inidividuaizone della versione del plugin grazie a semplici test sul tipo implementato, ma le potenzialità offerte dalla classe base sul tema di upgrade del sistema mi sembrano davvero valide se si considera la conservazione dell'esistente.

E il limite riguardo al problema tecnico di un unica estensione? Quale potrebbe essere il workaround in tal senso? In tal caso la classe pluggabile al sistema dovrebbe diventare una facciata, proxy o wrapper che dir si voglia. Per meglio capire quello che intendo facciamo un esempio. Ho esigenza che il mio DataProvider sia un ServicedComponent!

public class MyServicedComponent: ServicedComponent
{
    //TODO: Implementare i metodi di PersonsDataProvider
}
public class MyPersonsDataProvider: PersonsDataProviderBase
{
    private string configurationString;
    
    public override void Inizialize(string configurationString)
    {
        this.configurationString = configurationString;
    }    
    public override  PersonsCollection Retrieve()
    {
        using(MyServicedComponent component = new MyServicedComponent())
        {
            component.Inizialize(configurationString);
            return component.Retrieve();
        }
    }
    public override  Person RetrieveByCode(string Code)
    {
        using(MyServicedComponent component = new MyServicedComponent())
        {
            component.Inizialize(configurationString);
            return component.RetrieveByCode(code);
        }
    }
}
Technorati Tags:

posted @ lunedì 7 novembre 2005 11.02

Print

Comments on this entry:

# re: Archietture a plugIn, pensieri e considerazioni

Left by Lawrence Oluyede at 07/11/2005 13.25
Gravatar
Interessante, praticamente quello che Ruby ti offre by design con i Mixin:
http://www.rubycentral.com/book/tut_modules.html

Comunque concordo, meglio i plugin piuttosto che un'architettura pesantemente basata sull'inheritance. Ovvio che poi dipende dal caso...

# re: Archietture a plugIn, pensieri e considerazioni

Left by Andrea Raimondi at 07/11/2005 22.26
Gravatar
Ciao.

Secondo me la prima domanda da porsi è: mi serve davvero una architettura a plug-in?

Questa potrebbe sembrare una domanda retorica, ma in realtà non lo è poi tanto... mi spiego: una interfaccia a plug-in ha numerosi vantaggi, ma anche parecchi svantaggi, poiché perdi dei "collegamenti" interni che potresti altrimenti sfruttare. D'altro canto, l'uso di classi che vengono esposte al plug-in "funziona" fino ad un certo punto ed alla fine potresti trovarti ad averne di "troppo generiche" e rendere la vita molto più difficile al programmatore di estensioni - il quale a sua volta potrebbe essere meno invogliato a farlo.
Questo ovviamente viene considerato in un'ottica di distribuzione più amplia del software, è chiaro che se si tratta di un prodotto inhouse la cosa è diversa.

Altro aspetto è invece l'uso di interfacce e classi base. Personalmente, l'approccio che preferisco è misto: una interfaccia generale da cui il programma accede, poi invece una classe base astratta o veramente minimale(tanto minimale da essere "inutilizzabile" da sola senza derivazione) e poi una classe "default" che contiene i comportamenti più comuni.

Questo approccio consente una granularità molto elevata: usi l'interfaccia se vuoi fare qualcosa di interamente diverso, la classe minimale se vuoi modificare le cose a fondo ma entro certi limiti, oppure usi la classe di default se ti interessano comportamenti "un pò" diversi ma non eccessivamente.

Cerco di chiarire un pò di più il mio pensiero: se si tratta di avere un provider database, puoi usare la classe minimale se si parla di un certo dataprovider, la classe di default se si tratta di un certo dataprovider con certe proprietà e l'interfaccia se vuoi cambiare dataprovider(è solo un esempio, ovviamente).

Stessa cosa dicasi ad esempio per le forms: puoi usare la form minimale(aka: vuota, ad esempio) per modificare le cose in profondità pur lasciando un certo margine di già pronto, la classe di default(magari con dei metodi già implementati) per metter su soltanto i controlli oppure l'interfaccia se vuoi cambiar tutto.

Spero che sia chiaro :D

Ciao,

Andrea

# Classe Base Oppure Interfaccia

Left by Pingback/TrackBack at 08/11/2005 8.48
Gravatar
Quando definisci una classe base stai applicando il ParadigmaDiProgrammazioneObjectOriented, quando definisci una interfaccia stai applicando il ParadigmaDiProgrammazioneBasataSuInterfacce. Un esempio concreto di un problema che ammette entrambe le possibilità e della ricerca della soluizione ottimale la trovi in questo post di MarcoBarzaghi.

# re: Archietture a plugIn, pensieri e considerazioni

Left by Gundam at 23/11/2005 18.34
Gravatar
Credo che il concetto di plug-in sia filosofico e non solo una semplice questione di uso non uso.

Chi non comprende le potenzialità dei Plug, probabilmente si ritroverà a breve con del codice vecchio e peggio ancora destinato a rimanere tale.

Chi ha iniziato programmare ieri pomeriggio tardi, del resto fa fatica a comprendere quanto sia importante avere delle applicazioni capici di crescere senza troppi compromessi. Dando non solo un senso "filosofico di open source", ma di ottime capicità di integrazione.

Chi sa prevedere gli scenari di domani non è un veggente ne uno che si para le chiap. ma qualcuno sa il fatto suo.


Your comment:



 (will not be displayed)


 
 
 
Please add 5 and 7 and type the answer here:
 

Live Comment Preview:

 
«febbraio»
domlunmarmergiovensab
2930311234
567891011
12131415161718
19202122232425
26272829123
45678910