Some lyrics

The longer I program, the more I like loosely coupled systems, which consist of a large number of disparate units (modules) that know nothing about each other, but assume the existence of others. Such systems should ideally be assembled like a constructor, without dependencies and without adapting to each other. Ideally, in the process of operating such a system, all the necessary tasks should be performed without stopping the system, simply introducing a new module into the world (for example, by stuffing a jar'nik in the classpath), and the system will immediately begin to interact with the new module.
In this connection, the paradigm of event-driven (or
event-oriented ) programming looks very attractive.
Its indisputable advantage is that you do not need to edit the existing code when adding or removing a module, the system continues to work non-stop, we will simply gain or lose some of the functionality.
It is necessary to make a reservation, the minus is its plus - unpredictable behavior during changes, as well as weakening of control over the system as a whole (who is to blame, why does it work wrong?) - unlike traditional programming, where the system reacts to such anomalies immediately and firmly indication of the reasons (the required module was not found).
Why change traditions
To some extent, event-oriented programming manifests itself everywhere - from modern multi-tasking operating systems to all kinds of frameworks. Instead of each participant (module) listening to all events, he subscribes only to those that he is interested in, thereby consuming less machine resources. In fact, even a method call on an object can be perceived as a synchronous sending-receiving of messages with a guarantee of delivery and receipt. So why extra complexity?
The answer is that everything works for any outcome. We are not interested in who received our message, how many recipients, and whether it will respond at all. We just gently notify. No one is interested - and all right. Interesting - that's fine, work further.
Realities
For example, the same event system in Java Swing. To listen to some events, for each component there are methods addXXXListener, removeXXXListener. For sending such messages there is a fireXXXEvent method. Everything is fine. But, let's say, we are writing our component in the same philosophy. And we want to send a variety of events or respond to them, with the preservation of encapsulation. Consequently, it is necessary every time to implement these methods for each XXX event, for each component ...
')
Decision
The code is always similar to disgust, so I would like to replace it with a few lines. I thought, and as a result, not one day I implemented a helper for such tasks. This will be a class with static methods that can be called from anywhere in the program. So what do we need?
First, we want to respond to any events. Let, for definiteness, our events will implement the standard java.util.EventObject interface, and listeners will implement the java.util.EventListener interface. So, on the one hand, we are not limited by anything, but on the other, as simple as possible is to link this with the AWT \ Swing event paradigm. Then, perhaps, the event subscription should look like this:
public static synchronized void listen(final Class<? extends EventObject> event, final EventListener listener);
A naive implementation looks like this: if (!Events.listeners.containsKey(event)) { Events.listeners.put(event, new ArrayList<EventListener>()); } @SuppressWarnings("unchecked") final List<EventListener> list = (List<EventListener>) Events.listeners.get(event); if (!list.contains(listener)) { list.add(listener); }
So we expressed our desire to be aware of the events of a subclass of EventObject, promising that we will implement the EventListener interface (it does not define methods, more on this later). But we will be notified accurately if a specific event occurs.
Further, for a clean exit, we need the ability to unsubscribe from the event, suitable
public static synchronized void forget(final Class<? extends EventObject> event, final EventListener listener);
And the trivial code: public static synchronized void forget(final Class<? extends EventObject> event, final EventListener listener) { if (Events.listeners.containsKey(event)) { Events.listeners.get(event).remove(listener); } }
And also for a completely clean exit (although there is a controversial point here):
public static synchronized <E extends EventObject> void forget(final Class<? extends EventObject> event);
And the natural code is: public static synchronized <E extends EventObject> void forget(final Class<? extends EventObject> event) { if (Events.listeners.containsKey(event)) { Events.listeners.get(event).clear(); Events.listeners.remove(event); } }
Good for quitting. It seems to be all. And, no, not everything - still it is necessary to notify one-time in many places about any particular events, it looks like this:
public static synchronized void fire(final EventObject event, final String method);
The code is single-threaded: public static synchronized void fire(final EventObject event, final String method) { final Class<? extends EventObject> eventClass = event.getClass(); if (Events.listeners.containsKey(eventClass)) { for (final EventListener listener : Events.listeners.get(eventClass)) { try { listener.getClass().getMethod(method, eventClass).invoke(listener, event); } catch (final Throwable e) { e.printStackTrace(); throw new RuntimeException(e); } } } }
The lack of implementation, of course, is that the processing is performed sequentially, and in the calling thread. For most applications, this approach should be enough, but, apparently, it is worthwhile to organize processing via AWTsh EventQueue or something similar. In the next versions I will fix it.
Another disadvantage is that throwing an exception in the handler throws an exception into the method call, and informing the listeners stops (because of one “dishonest” listener, some others may not receive the message). In the final version, I corrected this behavior for ignoring with standard logging, and optionally subscribing to exception events in handlers.
Now, instead of implementing methods, everything can look like in the demo (for example, I chose java.awt.event.ActionEvent):
final ActionListener listener = new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { System.out.println(e); } }; Events.listen(ActionEvent.class, listener);
The only inconvenience is that in the Events.fire method, you must also specify the name of the method in a string, and that it must take one argument - the object of our event. This happened because different listeners implement different methods of reacting to messages, and even one listener can have several such methods - according to the type of event (Like MouseListener defines several methods, for example, mouseOver and mouseOut, as well as others).
And finally, repentance: all methods are statically synchronized, it is necessary to replace ordinary collections with thread-safe ones. Reflection also slows down the work (in terms of nanoseconds, compared to a direct call of methods) - which occurs in a cycle when an event is initiated for each listener, but I think this is a necessary evil.
Crooked, uncomfortable and unnecessary
I myself liked this approach so much that I decided to share the library with the community (
https://github.com/lure0xaos/Events.git ). If you do not like it, put minuses, but in general I will be glad to constructive criticism in the comments and efficient proposals and discussions.