Interface vs annotation driven events

Few days ago I was on a crossroad to design and implement simple notification system for one project. I was decided to go with standard interface-driven setup as common approach in Java since beginning. However after some googling I came accros another very interesting solution, EventBus. It’s a simple, annotation-driven event utility that’s part of Google Guava library.

EventBus allows publish-subscribe-style communication between components without requiring the components to explicitly register with one another (and thus be aware of each other). It is designed exclusively to replace traditional Java in-process event distribution using explicit registration. It is not a general-purpose publish-subscribe system, nor is it intended for interprocess communication.

Interface-driven solution
Before dive into EventBus, let me show you how the typical interface-based notification system could look like. First we need to define a simple immutable object that should be transferred using event from producer to consumers (reused also in EventBus example):

public class Message {

    private final String sender;

    private final String subject;

    private final String text;

    public Message(String sender, String subject, String text) {
        this.sender = sender;
        this.subject = subject;
        this.text = text;
    }

    public String getSender() {
        return sender;
    }

    public String getSubject() {
        return subject;
    }

    public String getText() {
        return text;
    }
}

Than we need to create a listener and event that would carry an object of above Message type. Single listener must exists for each event (used very effective technique proposed by Laurent Simon when listener and event are bound to each other within event class):

public interface MessageRecievedEventListener {
    public void messageRecieved(Message msg);
}
public interface SystemEvent<L> {
	public void notify(L listener);
}
public class MessageRecievedEvent implements SystemEvent<MessageRecievedEventListener> {

    private final Message msg;

    public MessageRecievedEvent(Message msg) {
        this.msg = msg;
    }

    @Override
    public void notify(MessageRecievedEventListener listener) {
        listener.messageRecieved(msg);
    }
}

Now when we have an event and corresponding listener, we can create a consumer of above event:

public class MessageReceivedEventConsumer implements MessageRecievedEventListener {

    private static final Logger LOG = LoggerFactory.getLogger(MessageReceivedEventConsumer.class);

    @Override
    public void messageRecieved(Message msg) {
        LOG.info("messageRecieved(), msg: {}", msg);
    }
}

The only thing missing there is dispatcher. Dispatcher is a component responsible for registering consumers to particular event a firing events. After specific event is fired, all registered consumers will receive the exact event fired. In our implementation dispatcher is a black box since it knows nothing about specific event or listener, it works only with SystemEvent interface.

public class SystemEventBus {

    // ReentrantReadWriteLock could be used if synchronization has proven to be a bottleneck
    @SuppressWarnings("rawtypes")
    private final Multimap<Class, Object> eventBusListeners = Multimaps.synchronizedMultimap(HashMultimap.<Class, Object> create());

    public <L> void registerListener(Class<? extends SystemEvent<L>> eventClass, L listener) {
        eventBusListeners.put(eventClass, listener);
    }

    @SuppressWarnings("unchecked")
    public <L> void fireEvent(SystemEvent<L> event) {
        Collection<L> eventListeners = (Collection<L>) eventBusListeners.get(event.getClass());
        for (L listener : eventListeners) {
            event.notify(listener);
        }
    }
}

EDIT 08/2015: Sources including unit test are now available on GitHub.

Advantages:

  • static typing
  • dispatcher is a black box, it knows nothing about particular event or listener interface

Disadvantages:

  • necessary to create listener interface for each event
  • potential collision of method names in listener interfaces (when subscriber implements multiple listener interfaces)

Annotation-driven solution
On the other side, implementation of preceding example using EventBus is much easier. No specific interfaces are required. Listener has to only define public method, marked by Subscribe annotation with one parameter, the event that wants to capture.

public class MessageRecievedEvent {

    private final Message msg;

    public MessageRecievedEvent(Message msg) {
        this.msg = msg;
    }

    public Message getMsg() {
        return msg;
    }
}
public class MessageRecievedEventConsumer {

    private static final Logger LOG = LoggerFactory.getLogger(MessageRecievedEventConsumer.class);

    @Subscribe
    public void messageRecieved(MessageRecievedEvent e) {
        LOG.info("messageRecieved(), msg: {}", e.getMsg());
    }
}

Listener must be registered in EventBus instance in order to be notified when an event is fired.

Message msg = new Message("marlly", "Interface vs annotation driven events", "Post about differences between those event architectures");
MessageRecievedEvent msgEvent = new MessageRecievedEvent(msg);
new EventBus().post(msgEvent);

EDIT 08/2015: Sources including unit test are now available on GitHub.

Advantages:

  • less code
  • no specific listener interface for each event
  • can listen to event supertype and take advantage of inheritance
  • detects events that have attached no listeners

Disadvantages:

  • moot lack of static typing (register and post methods accept Object type as parameter)

Conclusion
If you are already utilizing Google Guava library and are looking for simple notification system, you should definitely use EventBus. For others just add Guava libray and use it too :). It’s really simple and effective way how to handle with events.

Post a Comment

Your email is never published nor shared.