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 |