Pull to refresh

Рецепт универсального слушателя (listener)

Reading time6 min
Views51K
Я часто и много работаю со Swing и, как следствие — очень часто приходится создавать
слушателей самых различных видов и форм. Однако некоторые виды встречаются чаще других,
и ниже я приведу свой рецепт автоматизации их создания.
Возможно, предложенный мной подход не оригинален, но в литературе я его не встречал.
UPD: спасибо pyatigil за ссылку на статью, в которой описывается аналогичный подход, но немного в другом стиле.

Суть проблемы


При создании интерфейсов на Swing или сложных структур изменяющихся данных (например, доменной модели объектов) часто приходится создавать observable-объекты, т.е. такие объекты, которые предоставляют интерфейс для уведомления всех заинтересованных подписчиков о своих изменениях. Обычно в интерфейсе такого объекта присутствуют методы вроде следующих:
/**
 * Регистрирует прослушивателя для оповещения об изменениях данного объекта
 */
public void addMyObjectListener(IMyObjectListener listener);
/**
 * Удаляет ранее зарегистрированного прослушивателя
 */
public void removeMyObjectListener(IMyObjectListener listener);

где IMyObjectListener — это интерфейс, определяющий возможности наблюдения за данным объектом, например:
public interface IMyObjectListener {
    public void dataAdded(MyObjectEvent event);
    public void dataRemoved(MyObjectEvent event);
    public void dataChanged(MyObjectEvent event);
}

При реализации описанной функциональности для оbservable-объекта необходимо:
  1. Хранить список слушателей (объектов типа IMyObjectListener) и управлять им, реализуя методы addMyObjectListener(IMyObjectListener) и removeMyObjectListener(IMyObjectListener)
  2. Для каждого метода, определенного в интерфейсе слушателя, предусмотреть метод типа fireSomeEvent(...). Например, для поддержки слушателей типа IMyObjectListener придется реализовать три внутренних метода:
    • fireDataAdded(MyObjectEvent event)
    • fireDataRemoved(MyObjectEvent event)
    • fireDataChanged(MyObjectEvent event)
    Все эти методы работают единообразно, рассылая уведомления о соответствующем событии всем зарегистрированным слушателям.
  3. Вызывать методы fireXXXX(...) везде, где необходимо оповестить подписчиков об изменениях.

Реализация десятка или двух десятков подобных объектов может быть довольно обременительной и нудной, т.к. приходится писать много одинакового по сути кода. Кроме этого, реализация observable-поведения содержит множество нюансов, которые необходимо учесть. Например:
  • Необходимо позаботиться о многопоточном использовании observable-объекта. Ни при каких обстоятельствах не должна нарушаться целостность списка слушателей и механизма их оповещения;
  • Как быть, если один из слушателей при обработке события выбросит исключение?
  • Необходимо отслеживать и как-то реагировать на циклические оповещения.

Все эти вопросы, конечно, решаемы, и любой программист сможет обойти все подводные камни, используя флаги, проверки, синхронизацию и т.п. Но когда, повторю, речь идет о написании одной и той же функциональности (с точностью до названий методов и интерфейсов) в десятке классов, все это становится довольно обременительно.
Данная статья описывает метод отделения функциональности поддержки слушателей из observable-объекта, основанный на динамическом создании прокси-классов. Полученный функционал можно будет без труда подключить к любому классу, не теряя при этом удобства использования и type-safety.

Идея решения


Рассмотрим упомянутый выше интерфейс IMyObjectListener и еще такой интерфейс:
public interface IListenerSupport<T> {
    /**
     * Регистрирует нового прослушивателя
     */
    public void addListener(T listener);
 
    /**
     * Удаляет ранее зарегистрированного прослушивателя
     */
    public void removeListener(T listener);
}

Что если у нас был бы класс, реализующий оба эти интерфейса следующим образом:
class MyObjectListenerSupport implements IMyObjectListener, IListenerSupport<IMyObjectListener> {
    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
    }
}

Тогда целевой класс можно было бы реализовать так:
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);
    }
}

В предложенном подходе вся логика по управлению списком слушателей и оповещению вынесена в отдельный класс и целевой observable-класс стал значительно проще. Если, к тому же, этот класс не предполагается в дальнейшем наследовать, то методы fireXXXX(...) можно вообще опустить, т.к. они содержат только одну строку кода, которая вполне информативна и может использоваться непосредственно.
В следующем подразделе будет показано как распространить этот подход на общий случай и не плодить постоянно классы типа XXXXListenerSupport.

Рецепт для общего случая


Для общего случая предлагается следующий подход, основанный на создании динамических прокси-классов.
Описывать особо ничего не буду, для большинства java-программистов тут и так все ясно.
public class ListenerSupportFactory {
 
    private ListenerSupportFactory() {}
 
    @SuppressWarnings("unchecked")
    public static <T> T createListenerSupport(Class<T> listenerInterface) {
        return (T)Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                new Class<?>[] { IListenerSupport.class, listenerInterface }, new ListenerInvocationHandler<T>(listenerInterface));
    }
 
    private static class ListenerInvocationHandler<T> implements InvocationHandler {
 
        private final Class<T> _listener_iface;
        private final Logger _log;
 
        private final List<T> _listeners = Collections.synchronizedList(new ArrayList<T>());
        private final Set<String> _current_events = Collections.synchronizedSet(new HashSet<String>());
 
        private ListenerInvocationHandler(Class<T> 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);
        }
    }
 
}

Вот собственно и все.
С помощью ListenerSupportFactory, имея только интерфейс слушателя IMyObjectListener, целевой observable-класс реализуется следующим образом:
public class MyObject {
 
    private final MyObjectListener listeners;
 
    public MyObject() {
        listeners = ListenerSupportFactory.createListenerSupport(MyObjectListener.class);
    }
 
    public void addMyObjectListener(IMyObjectListener listener) {
        ((IListenerSupport<MyObjectListener>)listeners).addListener(listener);
    }
 
    public void removeMyObjectListener(IMyObjectListener listener) {
        ((IListenerSupport<MyObjectListener>)listeners).removeListener(listener);
    }

    /**
      * Пример бизнес-метода с оповещением слушателей
      **/ 
    public void someSuperBusinessMethod(SuperMethodArgs args) {
        // todo: perform some cool stuff here
        // запускаем оповещение о событии
        MyObjectEvent event = new MyObjectEvent(); // инициализировали описание события
        listeners.dataAdded(event);
    }
 
}

Tags:
Hubs:
Total votes 37: ↑34 and ↓3+31
Comments49

Articles