I've been looking for a simple way to manage event passing between classes and, like many others, have found the whole addXxxxListener
paradigm to be tedious and overly verbose. I wrote up an interface with several default methods that seems to be working out fairly well for me, but before I decide to lean too heavily on it I felt it was a good idea to get some feedback.
My goals:
- Simplicity - Easy to implement and cover a vast majority of use cases
- Speed - Don't block the calling thread any more than is necessary
Basic code below. The Dispatcher
interface is the only thing under review: the rest is heavily simplified code used to form a sample use case.
The Main Interface - Dispatcher
import java.util.EventListener;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.ForkJoinPool;
import java.util.function.Consumer;
public interface Dispatcher<L extends EventListener>{
/**
* A set that contains all the event listeners attached
* to this object.
*
* @return A {@link Set} of event listeners ({@link L})
*/
Set<L> getEventListeners();
/**
* The {@link Executor} that handles event propagation.
*
* Defaults to {@link ForkJoinPool#commonPool()}.
*
* @see #dispatch(Consumer) for more information on
* how events are handled.
* @return The Executor that will handle the events.
*/
default Executor getEventExecutor(){
return ForkJoinPool.commonPool();
}
/**
* Adds an event listener to this object. All events
* announced to this object will be received by the
* provided listener ({@link L}) until it is
* {@link #removeEventListener(EventListener) removed}.
*
* Defaults to {@link #getEventListeners()}.{@link Set#add(Object) add(listener)}.
*
* @param listener
* The event listener to add.
*/
default void addEventListener(L listener){
getEventListeners().add(listener);
}
/**
* Removes an event listener from this object. Any
* events received after this action occurs will not be
* forwarded to this listener on a best-effort basis.
*
* Defaults to {@link #getEventListeners()}.{@link Set#remove(Object) remove(listener)}.
*
* @param listener
* The event listener to remove.
*/
default void removeEventListener(L listener){
getEventListeners().remove(listener);
}
/**
* Forwards an event to all listeners currently in the
* Set received from {@link #getEventListeners()}. The
* design intent is that, for a listener in the form
* {@code void onUpdate(Data data);}, this method can
* be invoked on all listeners by calling
* {@code dispatch(l->l.onUpdate(data));}.
*
* The default implementation submits a new call to
* {@link Consumer#accept(Object)} to the {@link Executor}
* provided by {@link #getEventExecutor()} for each
* event listener ({@link L}) in the backing collection
* ({@link #getEventListeners()}).
*
* @param action
*/
default void dispatch(Consumer<L> action){
getEventListeners().forEach(l->getEventExecutor().execute(()->action.accept(l)));
}
}
Setup
import java.io.IOException;
import java.net.URL;
import java.util.EventListener;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import Dispatcher;
public abstract class Downloader implements Dispatcher<Downloader.Listener>{
private Set<Listener> listeners = new CopyOnWriteArraySet<>();
@Override
public Set<Listener> getEventListeners(){
return listeners;
}
public abstract Response download(URL url) throws IOException;
public interface Listener extends EventListener{
void onDownloadStarted(Response response);
void onDownloadUpdate(Response response);
void onDownloadComplete(Response response);
void onDownloadError(Response response, Exception exception);
}
}
Dispatching
public Response download(URL url) throws IOException{
// ...
Response response = new Response(...);
dispatch(l->l.onDownloadStarted(response));
if(isInfiniteStream(connection)){
response.completeInfinite(...);
dispatch(l->l.onDownloadComplete(response));
return response;
}
try(BufferedInputStream inputStream = ...){
// ...
while((read = inputStream.read(buffer)) > -1){
response.update(...);
dispatch(l->l.onDownloadUpdate(response));
}
response.complete(...);
dispatch(l->l.onDownloadComplete(response));
return response;
}catch(IOException exception){
dispatch(l->l.onDownloadError(response, exception));
throw exception;
}
}
Listening
static final Downloader.Listener downloadConsoleLogger = new Downloader.Listener(){
@Override
public void onDownloadStarted(Response response){
System.out.println("Download started: " + response.getURL().toExternalForm());
}
@Override
public void onDownloadUpdate(Response response){
System.out.println("Update from " + response.getURL().toExternalForm() + ":\n\t" +
"cs:" + response.getCurrentSpeed() + " as:" + response.getAverageSpeed() +
"cb:" + response.getDownloadedSize() + " ab:" + response.getEstimatedSize());
}
@Override
public void onDownloadComplete(Response response){
System.out.println("Download complete: " + response.getURL().toExternalForm() +
"\n\t as:" + response.getAverageSpeed() +
" tb:" + response.getDownloadedSize());
}
@Override
public void onDownloadError(Response response, Exception exception){
System.err.println("Download failed: " + response.getURL().toExternalForm());
exception.printStackTrace();
}
};