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. |