MokaByte 50 - Marzo 2001
Foto dell'autore non disponibile
di
Sandro Pedrazzini
Frameworks e Patterns
A Caccia di Patterns
In questo articolo riprendiamo il discorso sulla relazione tra frameworks e patterns iniziata il mese scorso. Cerchiamo di trovare design patterns, conosciuti e non, all'interno del JDK, allo scopo di capire meglio e documentare l'architettura di alcune relazioni tra classi.

Introduzione
In un precedente articolo ([1]) abbiamo trattato il tema dei design patterns collegati alle architetture software a framework.
È stato detto che la comprensione dei patterns ha contribuito alla diffusione e al consolidamento della programmazione ad oggetti. I design patterns da soli spiegano perché la programmazione ad oggetti è stata un passo in avanti. Dimostrano cos'è la genericità e come va sfruttato al meglio il polimorfismo.
Una combinazione di patterns rappresenta un framework. Vale anche il contrario, cioè ogni framework può essere descritto attraverso un'interazione di pattern.
Nell'articolo precedente abbiamo visto, attraverso un paio di esempi estratti dal JDK, l'utilità dei patterns nella documentazione, o meglio, come sarebbe utile se alcune collaborazioni tra classi presenti nel JDK venissero descritte con i design patterns. In questo articolo procediamo a caccia di patterns, catalogati e non, all'interno del JDK.
 
 
 

Filtri di file
Un esempio interessante di pattern nel JDK ci è dato questa volta dall'utilizzo dei file, altro argomento relativamente ostico per chi si azzarda per la prima volta con il linguaggio di programmazione Java.
L'elemento particolare in questo caso è costituito dai filtri, ovvero dalla possibilità offerta nel JDK di filtrare la scrittura o la lettura di dati da file. Vediamo dapprima cosa si intende con filtro.
Siamo tutti abituati a costruzioni di questo tipo quando si tratta di creare un riferimento a un file:

PrintWriter out
   = new PrintWriter(new BufferedWriter(new FileWriter("foo.out")));

Si tratta di una costruzione ricorsiva. Quando in run-time viene inviato un messaggio all'oggetto out di tipo PrintWriter, il metodo selezionato delegherà parte della funzionalità al suo riferimento interno all'oggetto di tipo BufferedWriter, che a sua volta si occuperà di mandare la richiesta a FileWriter.
Un meccanismo del genere, realizzato con una combinazione di oggetti specifica, prevista nella definizione delle classi, può essere descritto dal pattern Proxy ([2]).
In realtà, però, nel nostro caso non si tratta di una combinazione specifica, prevista a priori, in cui un oggetto PrintWriter deve puntare a un oggetto BufferedWriter che a sua volta deve puntare a FileWriter. La combinazione è dinamica, stabilita unicamente in run-time, e può essere modificata in qualsiasi istante.
Questo lo si capisce osservando i costruttori utilizzati sopra, che accettano genericamente elementi di tipo Writer, cioè l'interfaccia implementata da PrintWriter, BufferedWriter e FileWriter.

public BufferedWriter(Writer out);
public PrintWriter(Writer out);

La combinazione è quindi completamente libera e permette l'aggiunta di nuove classi di output, ammesso che queste implementino l'interfaccia Writer. Il JDK, a questo proposito, facilita l'implementazione di filtri mettendo a disposizione la classe astratta FilterWriter (con la corrispondente FilterReader), la quale, oltre ovviamente ad essere un'implementazione di Writer, mette a disposizione la funzionalità minima di un filtro, tra cui il riferimento ad un oggetto di tipo Writer.
Unica cosa strana: le classi filtro concrete messe a disposizione dal JDK nelle gerarchie Reader e Writer non ereditano da FilterWriter e FilterReader, cosa che invece accade nelle gerarchie InputStream e OutputStream.
 
 
 

Pattern
Ma vediamo ora quale design pattern ci aiuta a descrivere questo meccanismo. Visto che il Proxy non è sufficiente, data la sua rigidità di combinazione, possiamo utilizzare il pattern Decorator [2].

Osserviamo inizialmente lo schema di classi:


Figura 1

Il pattern Decorator ha la caratteristica di avere oggetti che ereditano da un elemento in comune (Component) e che contemporaneamente hanno un riferimento ad elementi dello stesso tipo. La prima caratteristica permette la trasparenza di utilizzo, sfruttando il polimorfismo, mentre la seconda permette la delega delle operazioni di filtro (addedOperation()). Il fatto poi che il riferimento sia ad un oggetto di tipo Component serve alla dinamicità del meccanismo, che di nuovo sfrutta il polimorfismo per permettere un numero illimitato di combinazioni in run-time.
La dinamicità è l'elemento fondamentale di questo pattern. Il suo scopo è infatti quello di permettere l'estensione di singoli oggetti (non classi) con nuove funzionalità. Va utilizzato quando si desidera aggiungere o togliere nuove funzionalità in modo dinamico e trasparente. La possibilità di combinare ricorsivamente un numero illimitato di decoratori è d'aiuto quando l'utilizzo dell'ereditarietà porterebbe a un numero troppo elevato di sottoclassi, che non potrebbero in ogni modo prevedere tutti i casi possibili.
Vediamo ora il pattern Decorator applicato alla gerarchia di classi OutputStream.
 



Figura 2




Component e Container
Torniamo, come già fatto nel precedente articolo, alle classi presenti nel package java.awt per parlare del rapporto tra le classi Component e Container (dalla quale eredita anche JComponent, classe base per tutti i widgets di Swing).
Anche in questo caso il pattern associato ci permette di comprendere meglio la collaborazione tra le singole classi.
Il pattern coinvolto in questo caso è il Composite.
Vediamo lo schema di classi.


Figura 3

Questo design permette di trattare in modo uniforme oggetti individuali e composizioni di oggetti. È esattamente quanto capita con elementi di tipo Container nel JDK. Ogni Container è un Component è può contenere liste di Component, che a loro volta possono essere elementi finali della cerarchia di widgets (Button, Checkbox, ecc.) oppure di nuovo Container, continuando in modo ricorsivo la composizione.
I vantaggi principali di una visione uniforme consistono nel fatto che il codice del client rimane semplice, senza necessità di switch e flag per determinare con quale singolo componente ha a che fare. Inoltre nuovi componenti possono essere aggiunti in modo del tutto trasparente per il client.
Vediamo ora il pattern applicato alla gerarchia di classi per widgets e finestre in java.awt.
 
 


Figura 4




Altri patterns
Parecchi altri patterns del catalogo ([2]) possono essere estratti dal JDK. Oltre a quelli appena visti e a quelli trattati nell'articolo precedente ([1]), mi limito a citarne ancora un paio, senza entrare nel dettaglio dell'architettura.
Il primo è il pattern Bridge utilizzato per separare le due gerarchie di widgets: quella definita con elementi ComponentPeer, cioè le classi che interagiscono direttamente con il windowing system della piattaforma e quella definita con elementi Component. Questo design è fondamentale per i cosiddetti componenti "heavyweight" (tipici di AWT, contrapposti ai componenti "lightweight" presenti in Swing), perché permette l'adattamento, e di conseguenza il porting, delle classi AWT su diverse piattaforme, senza dover modificare le classi della gerarchia Component. 
Anche nel pattern Bridge, si nota l'utilizzo della composizione contrapposta all'ereditarietà. Lo scopo è quello di rendere maggiormente dinamica la relazione tra classi. La relazione gerarchica viene usata unicamente per ottenere il polimorfismo e poter quindi definire relazioni più generiche.

Collegato direttamente a questa relazione tra Component e ComponentPeer c'è il meccanismo di creazione dei widget "heavyweight" che passa dall'implementazione (specifica per ogni piattaforma) di sottoclassi della classe astratta Toolkit. Questa centralizza le operazioni di creazione dei widgets, fungendo da Abstract Factory (nome del pattern), mettendo a disposizione un'interfaccia per creare famiglie di oggetti correlati, senza ricorrere direttamente al nome della classe specifica a cui appartengono gli oggetti.
 
 
 

Nuovi patterns
Fin'ora ci siamo limitati ad estrarre patterns dal JDK seguendo come modello di riferimento il catalogo "ufficiale" sui design patterns, contenuto in [2].
In realtà qualsiasi modello, prassi di programmazione, o architettura ricorrente può essere interpretata come pattern.
In [1] abbiamo già fatto riferimento alla ricorrenza del modello add/remove/process utilizzato in Component per permettere alla classe di agire come Subject del pattern Observer a più livelli.
Qualcosa di simile si ha con il cosiddetto "get/set" pattern, cioè la prassi di chiamare getXyz e setXyz i metodi pubblici che permettono di accedere a variabili xyz con visibilità minore all'interno della stessa classe. Questa prassi è stata introdotta sistematicamente con classi del JDK di Java 2 soprattutto per questioni di compatibilità con il meccanismo dei Beans, componenti software Java che hanno la necessità di "mostrarsi" in modo uniforme a programmi esterni che ne vogliano fare uso. L'utilizzo sistematico del get/set pattern permette al Java Bean di comunicare il nome e il tipo di variabile a cui il programma esterno ha accesso.

Navigando all'interno della gerarchia di classi di Swing è possibile identificare un'ulteriore architettura ricorrente, denominata "interface/implementation" pattern. Si tratta di un'architettura interessante, usata nello sviluppo di framework in Java, utile, anche a scopi didattici, per capire la differenza nell'utilizzo di interfacce e classi astratte. È una sorta di parabola della programmazione a framework.
Analizziamo l'esempio della classe EtchedBorder in Swing. Questa è la sua situazione gerarchica:
 



Figura 5



L'interfaccia Border è la responsabile di tutte le interazioni con il resto del framework. Ogni componente del framework che utilizza un bordo lo fa attraverso l'astrazione Border, che rappresenta quindi la API di utilizzo per ogni bordo.
C'è con questo una chiara separazione tra interfaccia e implementazione, visto l'uso di "interface". L'interfaccia 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 AbstractBorder rappresenta invece un livello intermedio, una sorta di implementazione minima in comune tra tutte le classi concrete. Non solo: mette 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", cioè da classi direttamente utilizzabili dallo sviluppatore di applicazioni. Oltre a EtchedBorder si può trovare EmptyBorder, LineBorder, CompoundBorder, ecc.
La prassi per chi intendesse implementare una nuova classe di Border sarebbe di ereditare da AbstractBorder. Niente impedirebbe comunque di ereditare da una classe concreta esistente, nel caso si trattasse di una modifica minima.
Siccome un pattern del genere può rischiare di generare un numero molto elevato di tipi, sarebbe da utilizzare unicamente per astrazioni chiave del sistema, come del resto viene fatto in Swing. 
Un altro esempio in Swing consiste nell'astrazione TableModel-AbstractTableModel-DefaultTableModel.
 
 
 

Conclusione
In questo articolo abbiamo messo in pratica la ricerca di pattern iniziata in [1] per mostrare come sia più semplice, con modelli di riferimento, capire le collaborazioni tra classi e tra oggetti presenti nel JDK. Oltre ad alcuni patterns estratti dal catalogo pubblicato in [2], sono state mostrate altre architetture ricorrenti.
 
 

Bibliografia
[1] Pedrazzini Sandro: Framework e Patterns: Documentare con Pattern, Moka Byte, http://www.mokabyte.it, febbraio 2001.
[2] Gamma E., Helm R.Johnson R., Vlissides J.: Design Patterns, Elements of Reusable Object-Oriented Software, Addison Wesley, 1995.


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