Recipe for a universal listener

I often and a lot of work with Swing and, as a result - very often have to create
listeners of various types and forms. However, some species are more common than others,
and below I will give my recipe for automating their creation.
Perhaps the approach I proposed is not original, but I have not seen it in the literature.
UPD : thanks pyatigil for the link to the article , which describes a similar approach, but in a slightly different style.

The essence of the problem


When creating Swing interfaces or complex structures of changing data (for example, a domain model of objects), it is often necessary to create observable objects, i.e. objects that provide an interface for notifying all interested subscribers about their changes. Typically, the interface of such an object contains methods like the following:
/**
 * Регистрирует прослушивателя для оповещения об изменениях данного объекта
 */
public void addMyObjectListener(IMyObjectListener listener);
/**
 * Удаляет ранее зарегистрированного прослушивателя
 */
public void removeMyObjectListener(IMyObjectListener listener);

where IMyObjectListener is an interface that determines the ability to monitor this object, for example:
public interface IMyObjectListener {
    public void dataAdded(MyObjectEvent event);
    public void dataRemoved(MyObjectEvent event);
    public void dataChanged(MyObjectEvent event);
}

When implementing the described functionality for an observable object, you must:
  1. Keep a list of listeners (objects of type IMyObjectListener) and manage it by implementing the addMyObjectListener (IMyObjectListener) and removeMyObjectListener (IMyObjectListener) methods
  2. For each method defined in the listener interface, provide a method of type fire SomeEvent (...). For example, to support listeners of the IMyObjectListener type, you will have to implement three internal methods:
    • fireDataAdded (MyObjectEvent event)
    • fireDataRemoved (MyObjectEvent event)
    • fireDataChanged (MyObjectEvent event)
    All these methods work in the same way, sending notifications about the corresponding event to all registered listeners.
  3. Call fireXXXX (...) methods wherever it is necessary to notify subscribers about changes.

The implementation of a dozen or two dozen such objects can be quite burdensome and tedious, because you have to write a lot of essentially the same code. In addition, the implementation of observable behavior contains many nuances that must be taken into account. For instance:
  • You need to take care of the multithreaded use of the observable object. Under no circumstances should the integrity of the list of listeners and their notification mechanism be violated;
  • What if one of the listeners throws an exception when processing an event?
  • You need to track and somehow respond to cyclical alerts.

All these issues, of course, can be solved, and any programmer will be able to get around all the pitfalls, using flags, checks, synchronization, etc. But when, I repeat, it is about writing the same functionality (up to the names of methods and interfaces) in a dozen classes, all this becomes quite burdensome.
This article describes a method for separating listener support functionality from an observable object, based on the dynamic creation of proxy classes. The resulting functionality can be easily connected to any class without losing ease of use and type-safety.

Solution idea


Consider the IMyObjectListener interface mentioned above and another such interface:
public interface IListenerSupport {
    /**
     * Регистрирует нового прослушивателя
     */
    public void addListener(T listener);
    /**
     * Удаляет ранее зарегистрированного прослушивателя
     */
    public void removeListener(T listener);
}

What if we had a class that implements both of these interfaces as follows:
class MyObjectListenerSupport implements IMyObjectListener, IListenerSupport {
    public void addListener(IMyObjectListener listener) {
        // todo: сохранить listener во внутреннем списке
    }
    public void removeListener(IMyObjectListener listener) {
        // todo: удалить listener из внутреннего списка
    }
    public void dataAdded(MyObjectEvent event) {
        // todo: оповестить всех слушателей о событии dataAdded
    }
    public void dataRemoved(MyObjectEvent event) {
        // todo: оповестить всех слушателей о событии dataRemoved
    }
    public void dataChanged(MyObjectEvent event) {
        // todo: оповестить всех слушателей о событии dataChanged
    }
}

Then the target class could be implemented like this:
public class MyObject {
    private MyObjectListenerSupport listeners = new MyObjectListenerSupport();
    public void addMyObjectListener(IMyObjectListener listener) {
        listeners.addListener(listener);
    }
    public void removeMyObjectListener(IMyObjectListener listener) {
        listeners.removeListener(listener);
    }
    protected void fireDataAdded(MyObjectEvent event) {
        listeners.dataAdded(event);
    }
    protected void fireDataRemoved(MyObjectEvent event) {
        listeners.dataRemoved(event);
    }
    protected void fireDataChanged(MyObjectEvent event) {
        listeners.dataChanged(event);
    }
}

In the proposed approach, all the logic for managing the list of listeners and notifications is placed in a separate class and the target observable class has become much simpler. If, in addition, this class is not supposed to be inherited in the future, then fireXXXX (...) methods can be omitted altogether, because they contain only one line of code, which is quite informative and can be used directly.
The next subsection will show how to extend this approach to the general case and not constantly produce classes like XXXXListenerSupport.

General Recipe


For the general case, the following approach is proposed, based on the creation of dynamic proxy classes.
I won’t particularly describe anything, for most java programmers everything is clear here.
public class ListenerSupportFactory {
    private ListenerSupportFactory() {}
    @SuppressWarnings("unchecked")
    public static  T createListenerSupport(Class listenerInterface) {
        return (T)Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                new Class[] { IListenerSupport.class, listenerInterface }, new ListenerInvocationHandler(listenerInterface));
    }
    private static class ListenerInvocationHandler implements InvocationHandler {
        private final Class _listener_iface;
        private final Logger _log;
        private final List _listeners = Collections.synchronizedList(new ArrayList());
        private final Set _current_events = Collections.synchronizedSet(new HashSet());
        private ListenerInvocationHandler(Class listenerInterface) {
            _listener_iface = listenerInterface;
            // todo: find a more sensitive class for logger
            _log = LoggerFactory.getLogger(listenerInterface);
        }
        @SuppressWarnings({"unchecked", "SuspiciousMethodCalls"})
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            String methodName = method.getName();
            // (1) handle IListenerSupport methods
            if (method.getDeclaringClass().equals(IListenerSupport.class)) {
                if ("addListener".equals(methodName)) {
                    _listeners.add( (T)args[0] );
                } else if ("removeListener".equals(methodName)) {
                    _listeners.remove( args[0] );
                }
                return null;
            }
            // (2) handle listener interface
            if (method.getDeclaringClass().equals(_listener_iface)) {
                if (_current_events.contains(methodName)) {
                    throw new RuleViolationException("Cyclic event invocation detected: " + methodName);
                }
                _current_events.add(methodName);
                for (T listener : _listeners) {
                    try {
                        method.invoke(listener, args);
                    } catch (Exception ex) {
                        _log.error("Listener invocation failure", ex);
                    }
                }
                _current_events.remove(methodName);
                return null;
            }
            // (3) handle all other stuff (equals(), hashCode(), etc.)
            return method.invoke(this, args);
        }
    }
}

That's all.
Using ListenerSupportFactory, having only the IMyObjectListener listener interface, the target observable class is implemented as follows:
public class MyObject {
    private final MyObjectListener listeners;
    public MyObject() {
        listeners = ListenerSupportFactory.createListenerSupport(MyObjectListener.class);
    }
    public void addMyObjectListener(IMyObjectListener listener) {
        ((IListenerSupport)listeners).addListener(listener);
    }
    public void removeMyObjectListener(IMyObjectListener listener) {
        ((IListenerSupport)listeners).removeListener(listener);
    }
    /**
      * Пример бизнес-метода с оповещением слушателей
      **/ 
    public void someSuperBusinessMethod(SuperMethodArgs args) {
        // todo: perform some cool stuff here
        // запускаем оповещение о событии
        MyObjectEvent event = new MyObjectEvent(); // инициализировали описание события
        listeners.dataAdded(event);
    }
}

Also popular now: