MokaByte 51 - Aprile 2001
Foto dell'autore non disponibile
di
Sandro
Pedrazzini
Realizzazione di un Framework
Primi Elementi di Design
In questo articolo introduciamo il tema dello sviluppo di frameworks, utilizzando I patterns come punto di partenza per decisioni riguardanti design e implementazione.

Introduzione
In due precedenti articoli ([1],[2]) dedicati al rapporto tra frameworks e design patterns abbiamo trattato il tema dei patterns come mezzo per documentare architetture software. Partendo dall'idea che ogni framework può essere descritto attraverso un'interazione di patterns, abbiamo analizzato il JDK per mostrare come diventerebbe più semplice la comprensione di alcune sue singole parti se queste venissero sistematicamente descritte in termini di patterns.

In questo articolo riprendiamo invece il discorso nella sua sequenza originale, cioè non più i patterns per descrivere un'architettura esistente, bensì i patterns come punto di partenza per decisioni riguardanti design e implementazione.

Esempio: Automi a stati finiti
L'esempio che consideriamo per trattare il design nasce dalla necessità di implementare un framework per la realizzazione di macchine a stati finiti (finite state machines, FSM). Punto essenziale dell'architettura a framework rispetto ad altre realizzazioni esistenti deve essere l'offerta di una struttura di partenza con parti flessibili che ne permettano un facile adattamento, sfruttando la tecnologia ad oggetti. Chi utilizza il framework non deve solo poter modificare le relazioni esistenti tra i vari stati della macchina (cosa facilmente realizzabile cambiando i dati di partenza, cioè prevedendo un documento in input che descriva la struttura particolare della macchina desiderata), ma deve poter adattare il meccanismo stesso che sta alla base della realizzazione.
In termini di framework object-oriented questo significa che l'architettura deve mettere a disposizione parti flessibili e adattabili (i cosiddetti hot spots) che attraverso polimorfismo ed ereditarietà permettano la generazione di applicazioni diverse, basate sulla stessa architettura di base, cioè sullo stesso framework.

Prima di iniziare con la spiegazione dell'architettura vediamo alcune nozioni di base su FSM e frameworks. Per una descrizione più dettagliata e formale dei primi, trattati in tutti i corsi di informatica, rimando a [3], mentre per i secondi rimando a [4] e [5].
 
 
 

Automi a stati finiti
Un automa a stati finiti è il modello matematico di una macchina in grado di accettare un insieme di sequenze, tratte da un alfabeto predefinito. Le implementazioni di automi a stati finiti considerano spesso unicamente le lettere come possibile alfabeto. Il nostro framework prevede invece la possibilità di definire come alfabeto qualsiasi insieme di oggetti.
La rappresentazione grafica più comune degli automi è quella del transition diagram. Questo contiene la rappresentazione degli stati (cerchi) e le possibili transizioni (frecce) per passare da uno stato all'altro. Il cerchio con bordo doppio rappresenta uno stato finale.
L'automa rappresentato nella figura seguente accetta (yes/no) qualsiasi sequenza di "0" e "1", in cui il simbolo "1" compare un numero dispari di volte.
 




Quando la macchina a stati finiti non serve unicamente ad accettare una sequenza in input, ma contemporaneamente genera anche un output, direttamente dipendente dalla sequenza in entrata, si parla di transducer.
In un transducer ogni transizione presenta un simbolo di input e un simbolo di output. Un transducer può essere ad esempio utilizzato per gestire informazioni di tipo morfolinguistico, come mostrato dalla figura che segue. Ogni transizione presenta un valore in input e, separato da "/", un valore in output.



Da notare che un transducer del genere permette la lettura sia in input che in output.
 
 
 

Framework
Come già anticipato in [1], parliamo di framework quando abbiamo una serie di classi che collaborano tra di loro formando un design riutilizzabile per una specifica classe di software. Lo scopo di un framework è quello di definire la struttura di base di un'applicazione, classi e oggetti, e, soprattutto, la sequenza principale di un programma. Questi parametri iniziali permettono al programmatore di concentrarsi su parti specifiche della sua applicazione. Costruirà infatti l'applicazione utilizzando classi del framework o definendo sottoclassi di classi preesistenti, adattando, in altre parole, il framework nei suoi punti "flessibili", anche chiamati "hot spots".
Un framework può essere visto come un programma astratto. Solo la definizione di alcune classi concrete permette di generare un'applicazione. Il vantaggio consiste nel fatto che le decisioni più importanti riguardanti il design sono già state prese, permettendo uno sviluppo più veloce e una migliore manutenzione.
Introduciamo qui il ruolo di sviluppatore di applicazioni inteso come programmatore che "adatta" il framework per realizzare applicazioni specifiche.
 
 
 

Prima astrazione: transizione
La prima generalizzazione che vogliamo realizzare è quella riguardante i possibili alfabeti, in altre parole il tipo di contenuto delle transizioni (transitions, arcs).
Ogni elemento di tipo Arc contiene la coppia input/output (o solo input, nel caso di una macchina semplice, considerata quindi un tipo particolare di transducer). Si tratta di un'astrazione "chiave", perché ogni applicazione che utilizzi il framework come struttura portante, deve avere la possibilità di definire suoi tipi specifici di input e output (adattamento del framework, customization).
Per questo tipo di astrazione utilizziamo il pattern interface/implementation, già trovato in Swing e analizzato e spiegato in [2]. Si tratta di un pattern molto utile nelle architetture a framework: lo useremo per un paio di altre astrazioni "chiave". Questo pattern realizza i tre livelli di astrazione: interfaccia, classe astratta, classi concrete.
 




L'interfaccia Arc rappresenta la parte stabile del framework. Dev'essere fissa perché una sua modifica rappresenterebbe una catena di cambiamenti e adattamenti all'interno del framework e delle applicazioni derivate. È l'uso di "interface" che permette il riutilizzo del design.
La classe astratta AbstractArc rappresenta invece un livello intermedio, una sorta di implementazione minima in comune tra tutte le classi concrete. Mette inoltre a disposizione un'implementazione di default, permettendo quindi allo sviluppatore che volesse realizzare la classe concreta di concentrarsi sui metodi veramente necessari per differenziare la sua implementazione da altre. C'è a questo livello un riutilizzo parziale sia di design che di implementazione.
L'ultimo livello è quello dell'implementazione vera e propria, che in un framework è costituita da un certo numero di classi cosiddette "out of the box", nel nostro esempio riportiamo TransArc, l'elemento arc di un transducer, e DfaArc, l'elemento arc di una macchina a stati finiti semplice (dfa: deterministic finite-state automaton).
 
 
 

Interfaccia
In interfaccia si tratta di definire il minimo comune denominatore di operazioni su arc utilizzate in tutto il framework.

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); 
}
 

Le operazioni di Arc servono a gestire in modo generico le relazioni tra input e output e ad interfacciare questi elementi con il resto del framework. La generalizzazione dei singoli elementi in Java è molto semplice, vista la presenza della classe Object.
 
 
 

Livello astratto
Il ruolo della classe astratta nel pattern interface/implementation è quello garantire una base minima di funzionalità per lo sviluppatore di applicazioni.

public abstract class AbstractArc implements Arc { 
    protected Object input = null, output = null; 

    public Object getInput(){ 
        return input; 
    } 

    public Object getOutput(){ 
        return output; 
    } 

    public boolean equal(Arc other){ 
        return (compareInputTo(other.getInput()) == 0) && 
               (compareInputTo(other.getOutput()) == 0); 
    } 

    private int getInputHash(){ 
        if (input != null) 
           return input.hashCode(); 
        return 0; 
    } 

    private int getOutputHash(){ 
        if (output != null) 
           return output.hashCode(); 
        return 0; 
    } 

    public boolean equals(Object obj){ 
        return (obj instanceof Arc) && equal((Arc)obj); 
    } 

    public int hashCode(){ 
        return getInputHash() ^ getOutputHash(); 
    } 
}
 

Ecco alcuni commenti su questa classe.
 

  • Le variabili input e output vengono già definite a questo livello per facilitare il compito dello sviluppatore di applicazioni.
  • Solo tre metodi implementano i metodi definiti in interfaccia: equal(), getInput() e getOutput().
  • Due metodi privati sono stati aggiunti unicamente per aggiungere un controllo interno alla classe sui valori nulli di input e output.
  • I due ultimi metodi pubblici (equals() e hashCode()) riscrivono la funzionalità ereditata da Object nel caso in cui si dovesse utilizzare elementi di tipo Arc in combinazione con tabelle hash.
Anche questi due metodi possono essere riscritti dallo sviluppatore di applicazioni.
La reimplementazione di equals() è essenziale per garantire il controllo di uguaglianza all'interno dell'infrastruttura di liste del JDK. Queste utilizzano infatti equals() come operatore discriminante.
Ogni volta che si cambia equals() bisognerebbe anche adattare hashCode(), in modo da mantenere vera la relazione:

if (a.equals(b)) 
then (a.hashCode() == b.hashCode()) 

La negazione non è vera, cioè se due oggetti non sono uguali, questo non significa che i loro valori hashCode() siano necessariamente differenti.
È vera però la relazione:

if (a.hashCode() != b.hashCode()) 
then (!a.equals(b))

 Il valore hashCode() serve quindi a migliorare l'efficienza, perché è il primo elemento analizzato in una tabella hash per verificare l'uguaglianza, o, in altre parole, per determinare la posizione. Se due elementi hanno codice hash diverso, sono considerati diversi.
 L'implementazione di hashCode() nella classe astratta serve a garantire una buona approssimazione, ma può essere riscritta dallo sviluppatore di applicazioni, nel caso trovasse una formula più adatta al suo caso concreto.
 
 
 

Livello concreto
Il pattern in questione richiede che ci sia un livello concreto che rappresenti il cosiddetto "out of the box", cioè la classe concreta pronta per l'utilizzo.
Supponiamo di voler realizzare come applicazione del framework un transducer che abbia come alfabeto dei caratteri.
Un buon utilizzo del pattern deve servire a implementare nella classe concreta solo i metodi direttamente legati alla scelta del tipo di dati per l'alfabeto, nel nostro caso il tipo Character. Tralasciamo le implementazioni di read() e write(), perché non necessarie per la comprensione del design.

public class TransArc extends AbstractArc 

    public int compareInputTo(Object otherInput){ 
        return ((Character)input).compareTo(otherInput); 
    } 

    public int compareOutputTo(Object otherOutput){ 
        return ((Character)output).compareTo(otherOutput); 
    } 

    public void write(OutputFileWrapper outputStream){ 
        ... 
    } 

    public Arc read(InputFileWrapper inputStream){
        ...
    }
}
 

Conclusione
In questo articolo abbiamo introdotto l'idea di framework e l'utilizzo di patterns come punto di partenza per la definizione del design. Dopo aver specificato il tipo di problema che vogliamo affrontare, abbiamo realizzato una prima semplice astrazione attraverso il pattern interface/implementation. Con questa astrazione abbiamo potuto creare una generalizzazione sul tipo di dati utilizzato dal framework.
Nel prossimo articolo ci concentreremo su altri due elementi adattabili: l'attraversamento della macchina a stati finiti e l'estrazione di informazioni dalla stessa.
 
 
 

Bibliografia
[1] Pedrazzini Sandro: Frameworks e Patterns: Documentare con Patterns, Moka Byte, http://www.mokabyte.com, Febbraio 2001.
[2] Pedrazzini Sandro: Frameworks e Patterns: A Caccia di Patterns, Moka Byte, http://www.mokabyte.com, Marzo 2001.
[3] Carrol John, Long Darrel: Theory of Finite Automata, Prentice-Hall, 1989.
[4] Gamma E., Helm R.Johnson R., Vlissides J.: Design Patterns, Elements of Reusable Object-Oriented Software, Addison Wesley, 1995.
[5] Pedrazzini Sandro: The Jacaranda Framework, http://a.die.supsi.ch/~pedrazz/jacaranda
 
 

Sandro Pedrazzini ha studiato ingegneria informatica al politecnico di Zurigo e ha ottenuto il Ph.D. all'università di Basilea. Da anni si occupa di progettazione e sviluppo object-oriented. Attualmente collabora con l'università di Basilea in progetti di trasferimento tecnologico e tiene corsi di design e sviluppo software presso la SUPSI, Scuola Universitaria Professionale della Svizzera Italiana, a Lugano.

Vai alla Home Page di MokaByte
Vai alla prima pagina di questo mese


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
mokainfo@mokabyte.it