MokaByte 51 - Aprile  2001
Foto dell'autore non disponibile
di
Andrea Gini
Sviluppo della GUI in applicazioni 
Java stand alone 
Parte II: riduzione di metodi lunghi
Nell'articolo del mese scorso abbiamo introdotto, come esempio di programma a finestre, un semplice editor di testo; quindi ne abbiamo presentato una prima implementazione, seguendo un modello di sviluppo abbastanza comune. Il sorgente risultante presentava alcuni difetti strutturali, tra i quali l'eccessiva lunghezza dei metodi. Un simile problema può essere trascurato su programmi mono-uso di piccole dimensioni, mentre può creare grossi problemi su programmi grafici destinati a crescere nel tempo. In questo articolo illustreremo come superare, grazie al Refactoring, questo problema

Il primo problema da affrontare, in un'applicazione che tende a crescere, è la dimensione del costruttore. Abbiamo visto che implementare la generazione di un'interfaccia grafica ricorrendo ad un unico metodo non è generalmente una buona idea. Il primo passo da compiere è quello di raggruppare le istruzioni in blocchi omogenei, in modo da suggerire una prima forma di organizzazione del codice.

Nel sorgente presentato nello scorso articolo, ad esempio, erano stati delineati cinque blocchi: 

  • impostazione degli attributi della finestra
  • impostazione del comportamento della finestra
  • costruzione dei componenti
  • costruzione dell'interfaccia grafica
  • registrazione degli ascoltatori.


Questa sequenza di fasi si adatta abbastanza bene a descrivere la costruzione di qualunque tipo di interfaccia grafica, e per questa ragione può essere presa come punto di partenza nello sviluppo di altri programmi.

Il primo provvedimento che prenderemo è quello di creare un metodo per ognuno di questi blocchi, con un nome appropriato a ciò che il metodo svolge, copiare il codice corrispondente all'interno del metodo e infine sostituire il blocco iniziale di codice con la chiamata al nuovo metodo. L'operazione appena descritta è un'esempio abbastanza caratteristico di "Estract Method" (M. Fowler), un'azione di Refactoring che permette di ridurre notevolmente la complessità di metodi grossi.

Curiosamente le tecniche di estrazione di metodo si propongono di ridurre la complessità di un codice articolato aumentandone le dimensioni (ogni nuovo metodo richiede come minimo tre righe, a cui va aggiunta la riga della chiamata); per ottenere un buon risultato è importante formulare in modo appropriato il nome del nuovo metodo, facendo in modo che esso descriva in maniera non ambigua il proprio ruolo. Il risultato di questa operazione è quello di scaricare la complessità verso metodi periferici, mentre il metodo principale assume un'aspetto pulito e una sintassi "autodescrittiva".

Per riuscire a valutare l'impatto delle modifiche, si consiglia di tenere a portata di mano un tabulato del sorgente pubblicato nell'articolo precedente.

Il nostro costruttore è formato dai cinque blocchi descritti sopra; da tali blocchi possiamo estrarre i seguenti metodi: 

  • setupFrame()
  • setupFrameBehaviour()
  • setupComponents()
  • buildGui()
  • registerListeners()


Applicando la modifica, otteniamo un immediato miglioramento:

  ....
  public TextEditor() {
    super();
    setupFrame();
    setupFrameBehaviour();
    setupComponents();
    buildGui();
    registerListeners();
  }
  protected void setupFrame() {
    setTitle("TextEditor");
    setSize(300,300);
  }
  protected void setupFrameBehaviour() {
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
  }
  protected void setupComponents() {
    editor = new JTextArea();
    fileChooser = new JFileChooser();

    OpenMenuItem = new JMenuItem("Open",new ImageIcon("Open24.gif"));
    SaveMenuItem = new JMenuItem("Save",new ImageIcon("Save24.gif"));
    ....
    OpenButton = new JButton(new ImageIcon("Open24.gif"));
    ....
  }
  ....

Partendo da un unico costruttore di 50 righe, ci ritroviamo con un costruttore di appena 6 righe, più cinque metodi di circa 10 righe ognuno. Ogni metodo ha un compito ben preciso, e pertanto se in futuro dovessimo apportare delle modifiche andremo ad agire solamente sul metodo che ci interessa, senza doverci preoccupare degli altri.
 
 
 

Andiamo in profondità
L'estrazione di metodo può essere adottata anche ad un livello di granularità maggiore: ad esempio il metodo setupComponents() può essere a sua volta semplificato estraendo le procedure che creano i pulsanti e quelle che creano i MenuItems: 

  protected void setupComponents() {
    editor = new JTextArea();
    fileChooser = new JFileChooser();
    buildButtons();
    buildMenuItems();
  }
  protected void buildButtons() {
    OpenMenuItem = new JMenuItem("Open",new ImageIcon("Open24.gif"));
    SaveMenuItem = new JMenuItem("Save",new ImageIcon("Save24.gif"));
    ....
  }
  protected void buildMenuItems() {
    OpenButton = new JButton(new ImageIcon("Open24.gif"));
    ....
  }

Ci si può domandare fino a che punto sia opportuno spingersi con l'estrazione di metodo. Non deve stupire che in casi estremi si arrivi addirittura ad estrarre metodi di una singola riga: nel prossimo articolo descriveremo un particolare tipo di metodo che spesso non supera questa dimensione minimale. Nei casi più comuni è bene fermarsi ad un livello intermedio, in cui arriviamo a gestire una famiglia di metodi snelli e dal contenuto omogeneo, privo di ambiguità. Bisogna comunque sottolineare il fatto che tutte le tecniche di Refactoring devono essere valutate su base empirica: solo con l'esperienza si riesce ad affinare la capacità di distinguere tra una soluzione elegante ed una che non lo è. Se prendiamo l'abitudine di raffinare il nostro codice, tendiamo a diventare più critici, riusciamo a scoprire i nostri punti deboli ed impariamo a commettere meno errori. In breve tempo aumenta la capacità di produrre codice pulito, con il sorprendente risultato di abbattere drasticamente i tempi di debugging ed elevare il livello di produttività personale.

L'estrazione di metodo è una tecnica estremamente utile, che presenta tuttavia alcuni aspetti critici: nei prossimi paragrafi analizzeremo un paio di tali problemi.
 
 
 

Regole di accesso ai nuovi metodi 
Il primo dubbio che sorge quando si aggiunge un metodo per estrazione è quello di stabilirne la politica di accesso. Se definisco "public" tale metodo, offro pubblico accesso a dei metodi che dovrebbero restare nascosti. Se li definisco come "private" ho risolto il primo problema, ma di fatto restringo molto la possibilità di azione di chi desidera creare sottoclassi dell'oggetto. I metodi ottenuti per estrazione in questo esempio vengono marcati "protected": in questo modo risultano accessibili solamente alle sottoclassi, mentre rimangono invisibili in tutti gli altri casi. Come regola generale, quando si introduce un metodo ad esclusivo uso interno, è preferibile impostare un permesso di accesso di questo tipo, in modo da lasciare inalterata l'interfaccia di programmazione pubblica. Se in futuro si dovesse estendere l'accesso a tali metodi sarà sempre possibile farlo, ma in linea di principio è bene cercare di mantenere quanto più piccolo possibile l'insieme dei metodi public.
 
 
 

Variabili globali
Il secondo problema che emerge puntualmente quando si pratica l'estrazione di metodo è il seguente: come ci si deve comportare quando dal costruttore estraiamo una coppia di metodi che lavorano sulle stesse variabili? Nell'esempio descritto sopra, i metodi setupComponents() e buildGui() presentano proprio questo tipo di problema: il primo dei due metodi costruisce gli oggetti che verranno utilizzati dal secondo. Nel caso specifico è necessario creare degli attributi globali, che risultino visibili ad entrambi i metodi. Tali attributi andrebbero marcati come "private", e resi accessibili attraverso un apposito metodo getXXX():

    ....
  // Un attributo privato è visibile solo 
  // all'interno della classe 
    private JTextComponent editor;
  .... 
  protected void setupComponents() {
     .... 
     editor = new JTextArea();
     ....
  }
  // Il metodo getEditor() offre un'accesso read-only
  // all'attributo "editor" anche alle sottoclassi 
  protected JTextComponent getEditor() {
    return editor;
  }

  protected void buildGui() { 
    ....
    // la riga seguente utilizza l'attributo "editor" 
    // ricorrendo al metodo getEditor()
    getContentPane().add(BorderLayout.CENTER,new JScrollPane(getEditor())); 
    ....
  }

Si noti che anche in questo caso abbiamo utilizzato il modificatore "protected" sul metodo getEditor(), in modo da rendere accessibile l'attributo "editor" in modalità read-only anche alle sottoclassi. 

Una soluzione più semplice, ma certamente meno sicura, è quella di impostare il modificatore "protected" direttamente sugli attributi, rendendoli visibili globalmente anche alle sottoclassi. Questa tecnica viene generalmente sconsigliata, dal momento che offre la possibilità di creare sottoclassi che agiscono direttamente sull'attributo, violando il principio dell'incapsulamento. Nel sorgente di esempio imposteremo come "protected" gli attributi relativi ai pulsanti e ai MenuItems: 

  protected JMenuItem OpenMenuItem;
  protected JMenuItem SaveMenuItem;
  ....
  protected JButton OpenButton; 
  ....

  protected void setupComponents() {
    ....
    OpenMenuItem = new JMenuItem("Open",new ImageIcon("Open24.gif"));
("Paste24.gif"));
    ....
    OpenButton = new JButton(new ImageIcon("Open24.gif"));
    ....
  } 
  protected void buildGui() { 
    ....
    JToolBar toolBar = new JToolBar();
    toolBar.add(OpenButton); 
    ....
  }
 

Riduzione del metodo actionPerformed()
Le tecniche di estrazione di metodo appena descritte possono essere applicate facilmente anche al metodo actionPerformed() del nostro esempio:

  ....
  protected 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) {}
      } 
  }
  protected void save() {
    int response = getFileChooser().showSaveDialog(this);
    if(response==JFileChooser.APPROVE_OPTION) {
      try {
        File f = getFileChooser().getSelectedFile();
        Writer out = new FileWriter(f);
        editor.write(out);
      }
      catch(Exception e) {}
    } 
  }
  protected void cut() {
    getEditor().cut();
  }
  protected void copy() {
    getEditor().copy();
  }
  protected void paste() {
    getEditor().paste();
  }
 
  public void actionPerformed(ActionEvent ae) {
    if(ae.getSource().equals(OpenButton) || ae.getSource().equals(OpenMenuItem))
      open();
    else if(ae.getSource().equals(SaveButton) || ae.getSource().equals(SaveMenuItem))
      save();
    else if(ae.getSource().equals(CutButton) || ae.getSource().equals(CutMenuItem))
      cut();
    else if(ae.getSource().equals(CopyButton) || ae.getSource().equals(CopyMenuItem))
      copy();
    else if(ae.getSource().equals(PasteButton) || ae.getSource().equals(PasteMenuItem))
      paste();
  }

Anche in questo caso abbiamo creato quattro metodi protected che sviluppano le funzionalità offerte dall'editor, ottenendo una definizione più pulita del metodo ascoltatore. Questa operazione è il primo passo sulla strada di una corretta separazione tra sintassi e semantica: svilupperemo meglio questo tema in un prossimo articolo.
 
 
 

Conclusioni
In questo articolo abbiamo analizzato una tecnica di refactoring molto utile nello sviluppo di programmi grafici: l'estrazione di metodo. Attraverso tale tecnica siamo riusciti a ridurre la complessità della prima formulazione del programma, scaricando i dettagli verso metodi periferici molto specializzati. Nel prossimo articolo introdurremo il concetto di modularità, e impareremo come applicarlo ai programmi grafici.
 

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 descritto in questo articolo 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