La
magia delle interfacce grafiche è che esse donano ad un prodotto
immateriale (un pacchetto software) un involucro che crea l'illusione dell'interattività
tipica degli oggetti del mondo reale. Durante lo sviluppo, è importante
astrarre dalla struttura fisica dell'interfaccia e concentrarsi su ciò
che il programma deve fare, sorvolando sul come. Operare una simile distinzione
rende i programmi più flessibili, e capaci di adattarsi a nuove
esigenze.
Sintassi di un
programma grafico
La
sintassi di un programma grafico è data dall'insime dei suoi controlli
e dalle modalità di interazione con l'utente. In parole povere la
sintassi definisce le modalità in cui l'utente finale accede alle
operazioni del programma. La sua formulazione deve rispettare criteri ergonomici,
e per questa ragione spesso viene studiata in modo da ricordare quella
di un oggetto del mondo reale: ad esempio i media-player come WinAmp vogliono
riproporre la sintassi di un registratore a cassette, in modo da risultare
familiari fin dal primo utilizzo.
|
Figura
1 - la sintassi di WinAmp richiama quella di un registratore
Semantica di un
programma grafico
La
semantica di un oggetto grafico è ciò che il programma fa,
ovvero l'insieme delle sue funzionalità: manipolazione di testi,
compressione di files Zip, sintesi di files multimediali.... è la
parte del programma che racchiude la tecnologia vera e propria, e per questa
ragione è quella più impegnativa da sviluppare.
Problema: confusione
tra sintassi e semantica
Se
dal punto di vista sintattico WinAmp assomiglia ad un registratore a cassette,
possiamo stare certi che il suo funzionamento interno è estremamente
diverso. Sotto al "guscio" costituito dalla sua interfaccia grafica non
troviamo di certo nastri magnetici e testine analogiche, ma una libreria
di funzioni per la manipolazione di files musicali. L'interfaccia grafica
richiede all'utente i parametri necessari, formula una chiamata ad una
funzione di libreria, e mostra il risultato a video.
Nel
corso dello sviluppo, capita spesso che la sintassi e la semantica di un
oggetto grafico vengano realizzate insieme, e che vengano sovrapposte ed
intrecciate in un modo non elegante. Questa sovrapposizione spesso è
la conseguenza di un uso improprio di ambienti di sviluppo visuale che,
al fine di rendere più rapido lo sviluppo, offrono parecchie scorciatoie
in tal senso.
Il
limite principale di questo approccio emerge nel momento in cui ci si trova
a dover operare delle modifiche all'interfaccia grafica per migliorare
l'aspetto dell'applicazione, o per aggiungere nuove funzionalità:
in queste fasi l'esistenza di un gran numero di dipendenze tra sintassi
e semantica costringe ad ore ed ore di hacking e di reverse engineering
per dipanare un insieme caotico.
La
soluzione a questo problema consiste nell'applicare in maniera corretta
alcuni principi fondamentali del Design e della Programmazione Orientata
agli Oggetti, come l'Incapsulamento e la Stratificazione.
Interfaccia di
programmazione: l'incapsulamento
|
Figura
2 - Incapsulamento
L'incapsulamento,
uno dei principi chiave su cui poggia la filosofia OO, è una metafora
di come sono organizzati gli oggetti del mondo reale. Ogni oggetto del
mondo reale ha un certo numero di attributi: alcuni sono immutabili, altri
possono essere modificati solo attraverso apposite operazioni che
rispettano l'integrità dell'oggetto. Una casa ha, tra i propri attributi,
il numero delle finestre ed il colore. Il primo di questi attributi è
immutabile: esso viene impostato stabilmente durante la costruzione, e
non è possibile modificarlo. Il colore è invece un attributo
che possiamo modificare in qualunque momento ridipingendo la casa. Questa
situazione può essere rappresentata dal seguente sorgente Java:
public
class Home {
private int windowNumber;
private Color color;
public int getWindowNumber() {
return windowNumber;
}
public Color getColor() {
return color;
}
public void paintHome(Color c) {
color = c;
}
}
Per
mettere in pratica l'incapsulamento, dobbiamo aver cura di definire "private"
gli attributi, e di fornire accesso ad essi solo ed unicamente attraverso
un apposito insieme di metodi pubblici. L'insieme dei metodi "public" costituisce
lIinterfaccia di Programmazione di un'oggetto, ossia l'insieme di operazioni
legalmente consentite su di esso. Se evitiamo di definire attributi "public",
siamo sicuri che l'interfaccia di programmazione è l'unica via di
accesso alla semantica dell'oggetto.
Violazione dell'incapsulamento
attraverso interfacce grafiche
|
Figura
3 - I metodi di dialogo con l'Utente aprono una breccia
Scrivendo
codice grafico diventa facile aggirare le protezioni offerte dall'incapsulamento.
Se non si presta attenzione, la manipolazione di alcuni attributi non passa
più attraverso i metodi designati, ma viene operata direttamente
dall'utente attraverso la manipolazione di un controllo grafico. Questo
modo di sviluppare portare errori ed inconsistenze, e per questo andrebbe
evitato; per raggiungere questo scopo, è importante riformulare
i metodi che implementano le operazioni del programma, operando una attenta
suddivisione dei compiti tra il gruppo dei metodi che manipolano i dati
e quelli che si occupano del dialogo con l'utente.
Architettura a
Strati
|
Figura
4 - La Stratificazione è un rimedio alla Violazione dell'Incapsulamento
Il
problema appena descritto può essere aggirato se si tiene presente
che gli oggetti grafici sono pur sempre oggetti Java: il fatto che siano
manipolabili con il mouse sullo schermo non esclude che essi debbano offrire
una via di accesso programmatica alle loro funzioni. Ogni funzionalità
del programma deve essere implementata anzitutto attraverso un metodo che
non faccia ricorso a componenti grafici per il dialogo con l'utente: un
tale metodo deve ricevere un insieme completo di parametri, e fornire in
risposta tutte le informazioni utili sull'esito dell'operazione, attraverso
un opportuno valore di ritorno o ricorrendo al meccanismo delle eccezioni.
Questo metodo sarà la base su cui in seguito verranno realizzate
le varie modalità di dialogo con l'utente.
|
Figura
5 - Interfacciamento tra due Strati
Questa
soluzione è un esempio di architettura a strati, l'aproccio progettuale
che sta alla base dei moderni Sistemi Operativi e dei Protocolli di Rete.
Interfaccia di
programmazione di TextEditor
Se
vogliamo definire una rigorosa interfaccia di programmazione per un oggetto
come il TextEditor, dobbiamo anzitutto creare un metodo pubblico per ognuna
delle funzionalità che esso mette a disposizione dell'utente:
public
void cutSelection()
public
void copySelection()
public
void pasteSelection()
public
void open()
public
void save()
Per
associare le funzionalità ai controlli grafici è sufficiente
fare in modo che questi metodi vengano richiamati dagli ascoltatori dei
pulsanti:
public
void actionPerformed(ActionEvent ae) {
if(ae.getSource().equals(OpenButton) ||
ae.getSource().equals(OpenMenuItem)) {
promptOpen();
}
else if(ae.getSource().equals(SaveButton) ||
ae.getSource().equals(SaveMenuItem)) {
promptSaveAs();
}
else if(ae.getSource().equals(CutButton) ||
ae.getSource().equals(CutMenuItem)) {
cutSelection();
}
else if(ae.getSource().equals(CopyButton) ||
ae.getSource().equals(CopyMenuItem)) {
copySelection();
}
else if(ae.getSource().equals(PasteButton) ||
ae.getSource().equals(PasteMenuItem)) {
pasteSelection();
}
}
I metodi
che implementano le operazioni su clipboard hanno una struttura piuttosto
semplice: nel caso specifico essi si limitano a chiamare un opportuno metodo
sulla JTextArea.
public
void cutSelection() {
getEditor().cut();
}
public void copySelection() {
getEditor().copy();
}
public void pasteSelection() {
getEditor().paste();
}
Le
funzioni "Open" e "Save" richiedono invece una maggiore attenzione. Vediamo
di analizzare in dettaglio l'implementazione della prima delle due:
public
void open() {
int response = getFileChooser().showOpenDialog(this);
if(response==JFileChooser.APPROVE_OPTION) {
try {
File f = getFileChooser().getSelectedFile();
Reader in = new FileReader(f);
editor.read(in,null);
}
catch(Exception e) {
JOptionPane.showMessageDialog(this,
"Errore di Input Output",
"I/O Error",
JOptionPane.WARNING_MESSAGE);
}
}
}
La
prima riga apre un JFileChooser, in modo da richiedere all'utente di segnalare
il file da caricare. Nella seconda riga si controlla che l'utente abbia
dato l'OK: in questo caso viene aperto il file e caricato il testo nell'editor.
Se durante la lettura qualcosa va storto, viene visualizzato un messaggio
di errore sullo schermo.
Questa
formulazione del comando "open", apparentemente corretta, ha esattamente
il difetto di creare un legame troppo stretto tra sintassi e semantica.
Infatti, per essere portata a termine, l'operazione necessita della collaborazione
dell'utente, al quale si richiede di operare su di un FileChooser. Questa
interazione è resa necessaria dal fatto che per portare a termine
l'operazione "Open" è necessario un parametro, in questo caso un
file, che è sconosciuto all'atto della chiamata. Tuttavia l'introduzione
di questo parametro nella funzione "Open" attraverso il FileChooser si
configura come una violazione dell'incapsulamento, dal momento che offre
all'utente la possibilità di inserire tipi di file non validi, come
ad esempio files eseguibili.
Se
vogliamo definire una chiara separazione tra la sintassi del comando "open"
(ossia il pulsante con il quale viene richiamata, il FileChooser con cui
l'operazione prosegue e JOptionPane con cui vengono segnalate le anomalie),
e la sua semantica (cioé il fatto che un file viene caricato all'interno
della TextArea) dobbiamo formulare una coppia di metodi: il primo di essi
deve essere in grado di portare a termine l'operazione senza alcun tipo
di intervento umano, il secondo deve fornire un front-end grafico al il
primo.
Semantica della
funzione Open
Dobbiamo
estrarre dalla precedente formulazione del metodo "open" la parte che costituisce
la semantica dell'operazione. La seguente procedura riceve come parametro
un file, e lo carica all'interno dell'Editor. Se durante il caricamento
si verifica un errore di Input Output, viene fatta rimbalzare una IOException,
se il file non è del tipo giusto viene lanciata invece una IllegalArgumentException.
public void open(File f) throws IOException,
IllegalArgumentException {
if(isValidFiletype(f)) {
Reader in = new FileReader(f);
// la riga seguente pu? generare una IOException
editor.read(in,null);
}
else
throw new IllegalArgumentException();
}
protected boolean isValidFiletype(File f) {
return f.getName().toLowerCase().endsWith(".txt");
}
Rimuovendo
il codice che svolge il dialogo con l'utente, abbiamo incapsulato la funzione
"open". Una simile formulazione è indipendente dal contesto di utilizzo,
pertanto nei prossimi due paragrafi potremo osservare due possibili implementazioni
per la sintassi: una grafica ed una a riga di comando.
Sintassi grafica
della funzione Open
Se
vogliamo definire un contesto grafico per la funzione "Open", possiamo
costruire un metodo che poggi sul precedente:
public void promptOpen() {
int response = getFileChooser().showOpenDialog(this);
if(response==JFileChooser.APPROVE_OPTION) {
try {
open(getFileChooser().getSelectedFile());
} catch(IllegalArgumentException e)
{
JOptionPane.showMessageDialog(this,
"Tipo di file non consentito",
"Wrong Filetype",
JOptionPane.WARNING_MESSAGE);
}
catch(IOException e) {
JOptionPane.showMessageDialog(this,
"Errore di Input Output",
"I/O Error",
JOptionPane.WARNING_MESSAGE);
}
}
}
Nella
prima riga vengono raccolti i parametri per la funzione "open", interrogando
l'utente con il FileChooser. A questo punto viene effettuata la chiamata
all'interno di un blocco try - catch: il blocco "try" definisce il comportamento
del programma in caso di esito positivo della chiamata, mentre i due blocchi
"catch" reagiscono alle due condizioni di eccezione mostrando gli adeguati
messaggi di errore.
Una sintassi a
riga di comando per la funzione Open
La
funzione "open" può tornare utile anche in contesti diversi da quello
presentato fino ad ora; in questo esempio vedremo come sia possibile utilizzarla
all'interno di un interprete di riga di comando, che permetta di forzare
il caricamento di un file di testo all'avvio del programma fornendone il
nome sulla riga di comando. Quello che vogliamo ottenere è che,
quando scriviamo
java TextEditor readme.txt
forziamo
l'editor a caricare il file "readme.txt" all'avvio. L'opzione di caricamento
da riga di comando ha la stessa semantica del tasto "open", ma chiaramente
ha un'altra sintassi. Avendo operato una netta distinzione tra i due aspetti,
è ora possibile utilizzare la stessa operazione in entrambi i contesti:
public
static void main(String argv[]) {
TextEditor t = new TextEditor();
if(argv.length==1) {
File f = new File(argv[0]);
try {
t.open(f);
}
catch(IllegalArgumentException e) {
System.out.println("Tipo di file non consentito");
System.exit(1);
}
catch(IOException e) {
System.out.println("Errore di I/O in lettura");
System.exit(2);
}
}
t.setVisible(true);
}
Questa
implementazione ha delle analogie con la precedente: dopo aver raccolto
i parametri, viene effettuata la chiamata alla funzione "open" dentro al
blocco "try". Se la chiamata va a buon fine, viene visualizzata la finestra
del programma, in caso contrario il codice all'interno dei blocchi "catch"
mostra un messaggio di errore sulla console e forza la terminazione.
Conclusioni
In
questo articolo abbiamo analizzato il problema della separazione tra sintassi
e semantica all'interno di un programma grafico. Abbiamo visto come viene
formulato il problema, e quali conseguenze possa avere nel corso dello
sviluppo. Abbiamo anche realizzato la separazione tra sintassi e semantica
all'interno del programma di esempio. Resta inteso che i benefici di una
simile modalità di sviluppo crescono in maniera smisurata nel momento
in cui il programma diventa grosso, e le funzioni che implementa aumentano
di complessità. Nel prossimo articolo verrà introdotta una
nuova funzionalità, che fornirà il pretesto per un ripasso
dei concetti esposti fino ad ora.
Bibliografia
I componenti
Swing
Andrea
Gini
Mokabyte
s.r.l.
www.mokabyte.com
"UML
Distilled" second edition
Martin
Fowler
ADDISON-WESLEY
Refactoring
: Improving the Design of Existing Code
by
Martin Fowler, Kent Beck (Contributor),
John Brant (Contributor), William Opdyke, don Roberts
Addison-Wesley
Object Technology Series
http://www.refactoring.com/
Design
Patterns: Elements of Reusable Object Oriented Software
by
Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides, Grady Booch
Addison-Wesley
Pub Co
L'esempio
con i sorgenti può essere trovato qui |