MokaByte 97 - Giugno 2005
  MokaByte 97 - Giugno 2005  

 

 

 

Gli ascoltatori d'evento

In questo articolo presentiamo una panoramica su quattro tecniche per la gestione degli eventi in Swing: implementazione diretta, classi interne, trampolini e azioni riflessive.

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.