MokaByte 58 - Dicembre 2001 

Realizzazione di un Framework
Gerarchie separate e costruttori virtuali

di
Sandro
Pedrazzini
Dopo le spiegazioni sugli elementi di design e l'adattabilità di un framework, apparsi in aprile e maggio 2001, aggiungiamo con questo terzo articolo due elementi fondamentali: la separazione delle gerarchie e la creazione di oggetti concreti.

Introduzione
Terminiamo idealmente con questa pubblicazione la serie di tre articoli sul tema dello sviluppo di un framework, iniziata nel mese di aprile 2001.
In questo intervento vedremo di spiegare alcuni elementi di dettaglio: il motivo dell'utilizzo di involucri (FileWrapper) per l'utilizzo di file, richiestomi dopo i due precedenti articoli, e come creare gli oggetti concreti senza che questi debbano essere conosciuti nel codice del framework.

 

File Wrapper

I file wrappers sono comparsi per la prima volta nella definizione dell'interfaccia Arc.

public interface Arc {
  public Object getInput();
  public Object getOutput();
  public boolean equal(Arc other);
  public int compareInputTo(Object otherInput);
  public int compareOutputTo(Object otherOutput);
  public void write(OutputFileWrapper outStream);
  public Arc read(InputFileWrapper inputStream);
}

L'interfaccia Arc viene utilizzata per la definizione della gerarchia degli archi della macchina a stati finiti, come mostra la seguente struttura [1]:


Figura 1


Vengono utilizzati durante le operazioni di input e output con file. La loro funzionalità non era stata mostrata perché non rilevante per il design di base. Li si poteva considerare semplicemente come rappresentanti di file.
Il motivo della loro presenza come wrapper diventa però evidente se consideriamo la possibilità di offrire più formati diversi dello stesso contenuto.
Limitiamoci all'operazione read(). Partiamo da un input di automa a stati finiti definito come file di testo. Per questo file abbiamo una sintassi che descrive il formato del testo e come questo formato deve essere interpretato da Arc e State durante il caricamento dei dati. Se ci fermassimo qui con l'analisi dei requisiti, non avrebbe senso usare un wrapper implementato da noi, basterebbe usare, invece di InputFileWrapper, un parametro di tipo Reader o di tipo InputStream. Basterebbe cioè lavorare direttamente con I file.
Non vogliamo però fermarci qui.
Mantenendo in superficie la medesima sintassi, desideriamo offrire la possibilità di leggere (e scrivere) dati in modo binario, o, più utile, dati crittati con uno dei molti algoritmi a disposizione.
Senza l'ausilio della classe wrapper, lo sviluppatore dovrebbe a questo punto estendere la gerarchia di Arc per poter prevedere la lettura del nuovo tipo di file.

 


In altre parole lo sviluppatore dovrebbe produrre un nuovo livello di gerarchia per un compito molto specifico, che non dovrebbe essere considerato un fattore decisivo nella distinzione di elementi di tipo Arc. Lo svantaggio di questo approccio diventa ancora più evidente se si considera che ci sono altri elementi responsabili di input/output: per tutti questi elementi bisognerebbe estendere la gerarchia e prevedere le diverse forme di input e output. Ci potrebbero inoltre essere elementi di tipo final, come ad esempio State, la cui gerarchia non può nemmeno essere estesa. Questo vuol dire che lo sviluppatore del framework dovrebbe prevedere ogni tipo di input/output, oppure, visto da un'altra angolazione, significa che le applicazioni sarebbero limitate ai tipi di input/output previsti dal framework.

 

Bridge Pattern
Un modo per evitare questa restrizione è quello di separare le due gerarchie e unirle con un riferimento a livello astratto. Questo è appunto lo scopo di InputFileWrapper e OutputFileWrapper.

Entrambe le interfacce presentano almeno una classe concreta.

 

Creazione di oggetti
In tutta l'implementazione del framework abbiamo lavorato il più possibile con interfacce e classi astratte. Lo scopo era quello di rendere il codice del framework stabile e indipendente dalle classi concrete utilizzate nelle applicazioni, cioè nelle istanze del framework stesso.
Si tratta ora di risolvere il problema della creazione degli oggetti concreti, senza ovviamente inserire il nome delle classi nel codice del framework, vanificando il lavoro di astrazione.
Il framework lavora con elementi astratti, ma in qualche modo a questi elementi astratti in run time devono essere associati oggetti concreti, che però non possono essere previsti nella struttura del framework.

 

Factory Method
Una soluzione a questo problema viene proposta dal pattern Factory Method ([4]), anche chiamato virtual constructor. Il pattern parte dal presupposto che il framework non conosce la classe concreta, ma conosce esattamente il momento in cui un oggetto di questa classe deve essere creato. Viene perciò chiamato un metodo che restituisce l'oggetto giusto al momento giusto.
Nel nostro caso utilizziamo una classe che contiene una lista di metodi factory. La classe è la versione concreta di una classe astratta (Abstract Factory) che ne definisce l'interfaccia.

public abstract class FsmTool {
  // Factory methods
  public InputFileWrapper createInputFileWrapper(String fileName){
    return new TextInputFileWrapper(fileName);
  }

  public OutputFileWrapper createOutputFileWrapper(String fileName){
    return new TextOutputFileWrapper(fileName);
  }

...
  public abstract Traversal createTraversal();
  public abstract Extractor createExtractor();
  public abstract Arc createArc();
}


Da notare che non tutti i metodi factory devono necessariamente essere astratti. La classe che funge da abstract factory può infatti definire elementi di default (nel nostro caso i due file wrappers). Solo gli ultimi tre metodi sono astratti, perché, come mostrato in [1] e [2], rappresentano l'essenza di questo framework.


Figura 4


Conclusione
Abbiamo così aggiunto una terza tappa al nostro tragitto attraverso lo sviluppo di framework. Si tratta di un meccanismo potente che ben utilizzato permette lo sviluppo di applicazioni affidabili in tempi abbreviati. L'individuazione di patterns all'interno dell'architettura aumenta la flessibilità e l'estensibilità del sistema, oltre che garantirne la documentazione. È utile ricordare che l'utilizzo di patterns non deve essere necessariamente previsto all'inizio del design. Infatti i patterns sono spesso l'obiettivo finale di iterazioni di refactorings [5].

 

Bibliografia
[1] Pedrazzini Sandro: Realizzazione di un Framework: Primi Elementi di Design, Moka Byte, http://www.mokabyte.com, Aprile 2001.
[2] Pedrazzini Sandro: Realizzazione di un Framework: Adattabilità, Moka Byte, http://www.mokabyte.com, Maggio 2001.
[3] Pedrazzini Sandro: The Jacaranda Framework, http://a.die.supsi.ch/~pedrazz/jacaranda
[4] Gamma E., Helm R.Johnson R., Vlissides J.: Design Patterns, Elements of Reusable Object-Oriented Software, Addison Wesley, 1995.
[5] Fowler Martin: Refactoring, Improving the Design of Existing Code, Addison Wesley, 1999.


MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems; tutti i diritti riservati 
E' vietata la riproduzione anche parziale 
Per comunicazioni inviare una mail a info@mokabyte.it