MokaByte 53 - Giugno 2001
Foto dell'autore non disponibile
di
Andrea Gini
Sviluppo della GUI in applicazioni 
Java stand alone 
Parte IV: Sintassi e Semantica 
nelle Interfacce Grafiche
I programmi grafici devono il loro successo al fatto che permettono di fare cose complicate in modo intuitivo, creando l'illusione di un ambiente interattivo. Un insieme di controlli grafici adeguatamente studiato permette di dominare la complessità di un'applicazione in modo molto più intuitivo di quanto avvenga nelle applicazioni a console. La modalità di sviluppo del codice applicativo può avere una grossa influenza nel definire la complessità di un'applicazione

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

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