Introduzione
La piattaforma ed il linguaggio Java consentono, in
genere, una molteplicità di approcci per la soluzione
di un identico problema, non solo, e non tanto, dal
punto di vista del modello concretamente applicato,
quanto sotto il profilo degli strumento concretamente
usati per la realizzazione. In questo articolo, approfittiamo
del Listener Pattern, usato dall'AWT/Swing (Java 1.2+)
per la gestione degli eventi, per osservare come il
problema della gestione degli eventi, e del suo coordinamento
con la struttura di un oggetto, possa essere risolto
in almeno quattro modi diversi, ognuno con i suoi pregi
e difetti.
Il
Listener Pattern
Il Listener Pattern si occupa di risolvere il problema
della gestione di un evento, generato da un componente,
da parte di un oggetto diverso dal generatore. Il modello
si basa su tre elementi. Un Tipo detto Ascoltatore di
Eventi (java.util.EventListener), un oggetto detto Evento
(java.util.EventObject) ed un oggetto detto Sorgente
dell'evento. I tre elementi possono essere così
sintetizzati:
/**
Rappresentazione di un evento */
public class Evento extends java.util.EventObject {
public Evento(Object source) {
super(source);
}
}
/**
Ascoltatore di eventi Evento */
public interface Ascoltatore extends java.util.EventListener
{
void eventoPerformed(Event e);
}
/**
Sorgente di un evento*/
public class Source {
ArrayList<Ascoltatore> ascoltatori = new ArrayList<Ascoltatore>();
public void addAscoltatore(Ascoltatore a) {
ascoltatori.add(a);
}
public void removeAscoltatore(Ascoltatore a) {
ascoltatori.remove(a);
}
public void fireEvento() {
Evento e = new Evento(this);
for(int i = 0; i < ascoltatori.size(); i++) {
a.get(i).eventoPerformed(e);
}
}
}
L'oggetto
sorgente, in reazione ad una situazione definita dal
programmatore, genererà un Evento e lo notificherà
a tutti gli ascoltatori registrati. È immediato
riconoscere lo stesso pattern, realizzato per la gestione
di un evento ActionEvent, generato da un pulsante JButton.
import
java.awt.*;
import java.awt.event.*;
import javax.swing.*;
/**
Il pattern listener in Swing */
public class Main implements Runnable, ActionListener
{
public static void main(String...args) {
SwingUtilities.invokeLAter(new Main());
}
public void run() {
JButton pulsante = new JButton("press me");
pulsante.addActionListener(this);
JFrame finestra = new JFrame("Prova");
finestra.add(pulsante);
finestra.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
finestra.pack();
finestra.setVisible(true);
}
public void actionPerformed(ActionEvent e) {
System.out.println("L'ascoltatore ha catturato
un evento");
}
}
Il
programma ha lo scopo di rappresentare l'uso da parte
di Swing del pattern Listener. Il pulsante JButton,
e come lui molti altri controli Swing, è una
sorgente di eventi ActionEvent. Per intercettare questo
tipo di eventi, è stato predisposto l'ascoltatore
ActionListener. ActionListener possiede un metodo, actionPerformed,
che è il metodo invocato dalla sorgete sugli
ascoltatori registrati, passando loro come argomento
un oggetto ActionEvent.
Gestione
degli eventi. Approcci ulteriori
Il
codice su riportato è solo uno dei molti modi
in cui è possibile applicare concretamente il
listener pattern. Ogni soluzione ha, come intuibile,
pregi e difetti. L'applicazione precedente ha il vantaggio
di sfruttare una classe già esistente per la
gestione delle azioni. L'associazione tra la sorgente
dell'evento e l'ascoltatore non richiede che siano generati
ulteriori oggetti. I difetti emergono qualora si esamini
la soluzione nell'ottica della prospettiva orientata
agli oggetti. Il codice violerebbe il principio di autonomia,
secondo cui ogni elemento del sistema a cui sia affidato
un compito, univocamente determinato, dovrebbe esistere
in forma di entità separata. In altri termini,
l'elemento, che si occupi di intercettare gli eventi
prodotti dai controlli, dovrebbe essere stigmatizzato
in un oggetto a sè stante. La classe Main, concretizzanto
il tipo ActionListener, rivece inoltre in dotazione
un comportamente (il metodo pubblico actionPerformed)
che non dovrebbe rientrare nel contratto della classe.
Per superare l'esposizione del metodo actionPerformed,
riportando la struttura dell'oggetto in un ambito strettamente
object-oriented, è comune l'uso di una classe
interna.
public
class Sample implements Runnable {
public static void main(String...args) {
SwingUtilities.invokeLater(new Sample());
}
public void run() {
JFrame f = new JFrame("Sample");
JButton button = new JButton("Go!");
button.addActionListener(new Ascoltatore());
f.add(button);
f.pack();
f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
f.setVisible(true);
}
private class Ascoltatore implements ActionListener
{
public void actionPerformed(ActionEvent e) {
System.out.println("Evento intercettato");
}
};
}
Il
codice su riprodotto elimina le questioni "strutturali".
La soluzione presenta comunque il problema della carenza
di specializzazione. Nell'unico metodo actionPerformed,
appartenente alla classe interna Ascoltatore, si concentrrebbe
la gestione degli eventi generati da una (probabile)
congerie di fonti. Per preservare la specializzazione,
si possono usare più ascoltari per più
generatori.
public
class Sample implements Runnable {
public static void main(String...args) {
SwingUtilities.invokeLater(new Sample());
}
public void run() {
JButton button1 = new JButton("Pulsante 1");
button1.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
button1Pressed();
}
});
JButton button2 = new JButton("Pulsante 2");
button2.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
button2Pressed();
}
});
JFrame f = new JFrame("Sample");
f.setLayout(new FlowLayout(FlowLayout.CENTER));
f.add(button1);
f.add(button2);
f.pack();
f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
f.setVisible(true);
}
public void button1Pressed() {
System.out.println("Pulsante 1 premuto");
}
public void button2Pressed() {
System.out.println("Pulsante 2 premuto");
}
}
Dal
punto di vista del linguaggio Java, l'esempio precendete
usa, come ascoltatori, delle istanze di classi interne,
locali, anonime. Ciò che importa è il
rispetto della struttura dell'oggetto (i metodo button1Pressed
e button2Pressed sono da interpretare come comportamenti
propri dell'oggetto Sample, eventualmente attivabili
attraverso l'interfaccia) e della specificità
delle sue funzioni. L'approccio, tuttavua, genera una
nuova classe per ogni ascoltatore e, in generale, una
classe per ogni controllo. L'alto numero di piccole
classi potrebbe causare un ritardo di risposta all'avvio
dell'applicazione, tanto più sensibile quanto
minori siano le prestazioni della macchina di esecuzione.
Inoltre, l'elevato numero di classi interne, locali,
anonime, appesantisce la lettura del codice, complessivamente
considerato.
I
trampolini (riflessivi)
Usando
un ascotatore di eventi, congiuntamente alla riflessione,
è possibile ottenere i vantaggi della gestione
separata degli eventi, conservando la pulizia del codice
sorgente, ricorrendo ad una sola definizione di classe.
La tecnica è definita "trampolino",
a segnalare il salto della gestione dell'evento dal
metodo che lo intercetta, contenuto nel trampolino,
ad un secondo metodo.
import
java.lang.reflect.*;
import java.awt.event.*;
public
class Trampolino implements ActionListener {
private Object riferimento;
private Method metodo;
public Trampolino(Object rif, String nomeMetodo) {
riferimento = rif;
try {
Class c = riferimento.getClass();
metodo = c.getMethod(nomeMetodo);
} catch(Exception e) {
throw new RuntimeException(e);
}
}
public void actionPerformed(ActionEvent e) {
try {
metodo.invoke(riferimento);
} catch(IllegalAccessException ex) {
throw new RuntimeException(ex);
} catch(InvocationTargetException ex) {
throw new RuntimeException(ex);
}
}
}
La
classe Trampolino concretizza il tipo ActionListener
ed è quindi registrabile come ascoltatore di
eventi ActionEvent. Per un caso d'uso, si consideri
il codice che segue.
import
java.awt.*;
import javax.swing.*;
public
class Main implements Runnable {
public static void main(String...args) {
SwingUtilities.invokeLater(new Main());
}
public void run() {
JButton button = new JButton("press me");
button.addActionListener(new Trampolino(this, "actionHandler"));
JFrame f = new JFrame("Sample");
f.getContentPane().add(button);
f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
f.pack();
f.setVisible(true);
}
public void actionHandler() {
System.out.println("evento intercettato");
}
}
Il
trampolino cattura l'evento generato dal pulsante button
e sposta il controllo al metodo actionHandler. L'effetto
è lo stesso ottenuto con la classe interna, locale,
anonima. Adottando un trampolino, oltre ad una maggiore
pulizia del codice, sfruttiamo le possibilità
offerte dallo spostamento del controllo approfittando
di una sola classe in più.
Le
azioni riflessive
Lo
stesso meccanismo dei trampolini, può essere
usato per creare azioni che sfruttino la riflessione
per contenere il numero di classi generate ed ottenere
un codice pulito.
import
java.awt.event.*;
import javax.swing.*;
import java.lang.reflect.*;
public
class ReflectiveAction extends AbstractAction {
private Method metodo;
private Object riferimento;
public ReflectiveAction(String aName, Object rif, String
nomeMetodo) {
super(aName);
try {
riferimento = rif;
Class c = riferimento.getClass();
metodo = c.getMethod(nomeMetodo);
} catch(Exception e) {
throw new RuntimeException(e);
}
}
public void actionPerformed(ActionEvent e) {
try {
metodo.invoke(riferimento);
} catch(IllegalAccessException ex) {
throw new RuntimeException(ex);
} catch(InvocationTargetException ex) {
throw new RuntimeException(ex);
}
}
}
Il
meccanismo è palesemente identico a quello dei
trampolini riflessivi, salva la richiesta di un parametro
in più, il primo nel costruttore di ReflectiveAction,
da usare come etichetta per l'azione. Il codice che
segue esemplifica l'uso di un'azione riflessiva.
import
java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public
class Sample implements Runnable {
public static void main(String...args) {
SwingUtilities.invokeLater(new Sample());
}
public void run() {
Action azione = new ReflectiveAction("Press me",
this, "buttonPress");
JButton button = new JButton(azione);
JMenuBar menuBar = new JMenuBar();
JMenu menu = menuBar.add(new JMenu("Menu"));
//usiamo la stessa azione del JButton per un pulsante
del Menu
JMenuItem pulsanteMenu = menu.add(azione);
JFrame f = new JFrame("Sample");
f.setJMenuBar(menuBar);
f.add(button);
f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
f.pack();
f.setVisible(true);
}
public void buttonPress() {
System.out.println("Pulsante premuto");
}
}
Conclusioni
Più della scelta, dell'una o dell'altra tecnica,
conta la consapevolezza delle conseguenze. Nelle poche
righe scritte, si è cercato di capire dove porti
ciascuno dei quattro modi presentati. L'uso concreto
dipende, anche, dalle preferenze di chi programmi. In
senso assoluto, le prestazioni migliori si ottengono
usando l'implementazione diretta dell'interfaccia-ascoltatore,
poichè non sono generate nè classi nè
oggetti ulteriori, rispetto all'unica concretizzazione
del Tipo ascoltatore. Le classi interne hanno il pregio
di consentire una migliore gestione della struttura
interna dell'oggetto, appesantendo il caricamento dell'applicazione
di quel (tanto o poco) necessario al ClassLoader per
caricare e risolvere un certo numero di classi in più,
rispetto alla soluzione dell'implementazione diretta.
L'uso della riflessione consente di ottenere un codice
più pulito, a parità di oggetti creati,
rispetto alle classi interne, creando un oggetto in
più per ogni controllo, rispetto all'implementazione
diretta. Inoltre, l'invocazione riflessiva di un metodo
è generalmente più "pesante"
rispetto all'invocazione diretta di un metodo attraverso
un reference. Quale sia la scelta adottata, lo ripetiamo,
ciò che importa è la consapevolezza degli
effetti.
Bibliografia
[1] Arnold, Gosling, Holmes, "The Java Programming
Language, 3th edition", 2002, Sun Micorosystem
Inc.
[2] Wilson, Kesselman, "Java Platform Performace,
Strategies and Tactice", 2001, Sun Microsystem
Inc.
|