Per una serie di motivi che non sto ad elencare, in quest’ultimo periodo mi sono dovuto confrontare un po’ con Java e tutto l’ecosistema che si porta dietro a partire da Eclipse fino ad Android passando per Apache Pivot.
L’esperienza non è stata affatto negativa, anche se dal punto di vista di un programmatore C#, Java sembra un po’ più spartano e “C like” (insomma un po’ di vecchiaia e polvere la noto).
Tuttavia in questo post mi voglio soffermare su un solo aspetto che ho trovato particolarmente “sconveniente” in Java e che invece trovo particolarmente comodo in C#: la gestione e dichiarazione di eventi “custom”.
Come primo step, in C# ho a disposizione il supporto nativo per i delegati che altro non sono che la rappresentazione tipizzata di un sano e vecchio puntatore a funzione. Quindi per definire un evento, inizio col dichiarare il delegato che ne rappresenta il suo handler:
public delegate bool MyEventHandler(int myEventData);
In Java non ho scelta, devo definire un’interfaccia ed usare una classe anonima per implementarla. Il delegato precedente si traduce quindi in questa interfaccia:
public interface MyEventListener { boolean onMyEvent(int myEventData); }
E fin qui direi che i due approcci siano piuttosto simili, anzi propendo un po’ di piu’ per l’approccio Java che trovo sicuramente elegante.
Il problema però è che per implementare un evento, oltre alla firma del metodo handler, devo gestire una lista degli oggetti che si sono sottoscritti ai quali inviare poi la notifica. In C# questo step si riduce a dichiarare l’evento ed automaticamente il compilatore si occupa di generare tutto il codice necessario “trasformando” il mio delegato in un delegato multicast, mantenendo così di fatto la lista dei vari sottoscrittori etc…
In pratica questo è il codice sufficiente, tutto il resto è automatico:
public event MyEventHandler MyEvent;
Semplicemente dichiarando l’evento posso sottoscrivermi, con gli operatori += e -= (pessima scelta da parte di Microsoft):
this.MyEvent += new MyEventHandler(this.OnMyEvent); this.MyEvent -= new MyEventHandler(this.OnMyEvent);
e notificare l’evento invocandolo:
if (MyEvent != null) MyEvent(10);
In Java non ho a disposizione nessuna facilitazione ed in pratica devo reimplementare per ogni evento custom le 3 funzioni necessarie al mantenimento della lista, usualmente nella forma:
addMyEventListener(MyEventListener listener); removeMyEventListener(MyEventListener listener); dispatchMyEvent();
E mantenere la lista dei sottoscrittori in un ArrayList o in altra collection (ad es. una EventListenerList che comunque trovo bruttina) in una variabile a livello di classe.
Dal punto di vista dell’utilizzo in Java posso sottoscrivermi utilizzando una classe anonima (notare il new dell’interfaccia MyEventListener)
addMyEventListener(new MyEventListener() { @Override public boolean onMyEvent(int myEventData) { // event handler } });
e per notificare l’evento è sufficiente chiamare la
dispatchMyEvent();
Non male, ma scrivere il trittico di funzioni add / remove / dispatch per ogni evento è tedioso e soggetto ad errori.
La domanda sorge spontanea: si può fare di meglio?
Premesso che “meglio” per me significa:
- Mantenere l’handler tipizzato. Quindi continuare ad usare un interfaccia per il “Listener”
- Mantenere le funzioni add / remove / dispatch tipizzate, ma senza doverle scrivere per ogni evento
- Non dover più dichiarare e gestire la collection dei listener in un field a livello di classe.
Che fare?
Lo spunto l’ho preso da C# 2.0 che con l’introduzione dei generics e di alcune altre classi di utilità ha snellito l’implementazione degli eventi, standardizzando i parametri del delegato “handler” senza sacrificarne però le potenzialità.
In pratica oggi li dichiaro quasi sempre “genericamente” in questo modo:
public class MyEventArgs : EventArgs { public int MyEventData = 0; // sarebbe meglio usare una proprietà } public class MyEventProducer { public event EventHandler<MyEventArgs> MyEvent; public void MyMethod () { if (MyEvent != null) MyEvent(this, new MyEventArgs()); } } public class MyEventConsumer { private MyEventProducer producer; public MyEventConsumer () { producer = new MyEventProducer (); producer.MyEvent += new EventHandler<MyEventArgs>(this.OnMyEvent); } private void OnMyEvent(object sender, MyEventArgs e) { // event handler } }
Dato che anche Java ha i generics, deve essere possibile realizzare qualche cosa di simile ed in effetti, anche se le mie conoscenze in materia sono vecchie solo di qualche giorno, implementando una semplice classe di utilità sono riuscito nell’intento.
public class EventArgs { public static final EventArgs Empty = new EventArgs(); } public interface EventListener<T extends EventArgs> { void onEventReceived(Object sender, T args); } public class Event<A extends EventArgs> { private List<EventListener<A>> listeners = new LinkedList<EventListener<A>>(); public void addListener(EventListener<A> listener) { listeners.add(listener); } public void removeListener(EventListener<A> listener) { while(listeners.remove(listener)) ; } public void dispatch(Object sender, A args) { for(EventListener<A> listener : listeners) { listener.onEventReceived(sender, args); } } } public class EventProducer { public final Event<EventArgs> simpleEvent = new Event<>(); public void run() { System.out.println("dispatching SimpleEvent..."); simpleEvent.dispatch(this, EventArgs.Empty); } } public class EventConsumer { private EventProducer producer; public EventConsumer() { producer = new EventProducer(); producer.simpleEvent.addListener(new EventListener<EventArgs>() { @Override public void onEventReceived(Object sender, EventArgs args) { System.out.println("SimpleEvent received"); } }); } public void run() { producer.run(); } public static void main (String[] args) { EventConsumer consumer = new EventConsumer(); consumer.run (); } }
In pratica il grazie alla classe generica Event ho mantenuto la forte tipizzazione dei vari metodi di gestione dell’evento sacrificando pochi aspetti quali:
- Il nome del metodo da implementare (sempre onEventReceived), ma poco male il nome dell’interfaccia dovrebbe essere sufficientemente esplicativo
- Non posso implementare 2 eventi distinti con la stessa firma a livello di classe, ma sono sempre costretto a passare per la classe anonima e poi eventualmente richiamare 2 metodi differenti dell’istanza “padre”.
Probabilmente alla lunga il punto 2 può causare qualche problema, ma per ora non vedo particolari controindicazioni, dato che l’uso di classi anonime mi sembra sia una pratica piuttosto diffusa in Java.
Qui il progetto eclipse completo con i miei esperimenti: CSharpLikeEvents.zip
E’ probabile che la soluzione proposta sia banale, già acquisita o completamente errata, ma cercando anche se non approfonditamente in rete non ho trovato nulla in proposito.