Я часто и много работаю со Swing и, как следствие — очень часто приходится создавать
слушателей самых различных видов и форм. Однако некоторые виды встречаются чаще других,
и ниже я приведу свой рецепт автоматизации их создания.
Возможно, предложенный мной подход не оригинален, но в литературе я его не встречал.
UPD: спасибо pyatigil за ссылку на статью, в которой описывается аналогичный подход, но немного в другом стиле.
При создании интерфейсов на Swing или сложных структур изменяющихся данных (например, доменной модели объектов) часто приходится создавать observable-объекты, т.е. такие объекты, которые предоставляют интерфейс для уведомления всех заинтересованных подписчиков о своих изменениях. Обычно в интерфейсе такого объекта присутствуют методы вроде следующих:
где IMyObjectListener — это интерфейс, определяющий возможности наблюдения за данным объектом, например:
При реализации описанной функциональности для оbservable-объекта необходимо:
Реализация десятка или двух десятков подобных объектов может быть довольно обременительной и нудной, т.к. приходится писать много одинакового по сути кода. Кроме этого, реализация observable-поведения содержит множество нюансов, которые необходимо учесть. Например:
Все эти вопросы, конечно, решаемы, и любой программист сможет обойти все подводные камни, используя флаги, проверки, синхронизацию и т.п. Но когда, повторю, речь идет о написании одной и той же функциональности (с точностью до названий методов и интерфейсов) в десятке классов, все это становится довольно обременительно.
Данная статья описывает метод отделения функциональности поддержки слушателей из observable-объекта, основанный на динамическом создании прокси-классов. Полученный функционал можно будет без труда подключить к любому классу, не теряя при этом удобства использования и type-safety.
Рассмотрим упомянутый выше интерфейс IMyObjectListener и еще такой интерфейс:
Что если у нас был бы класс, реализующий оба эти интерфейса следующим образом:
Тогда целевой класс можно было бы реализовать так:
В предложенном подходе вся логика по управлению списком слушателей и оповещению ��ынесена в отдельный класс и целевой observable-класс стал значительно проще. Если, к тому же, этот класс не предполагается в дальнейшем наследовать, то методы fireXXXX(...) можно вообще опустить, т.к. они содержат только одну строку кода, которая вполне информативна и может использоваться непосредственно.
В следующем подразделе будет показано как распространить этот подход на общий случай и не плодить постоянно классы типа XXXXListenerSupport.
Для общего случая предлагается следующий подход, основанный на создании динамических прокси-классов.
Описывать особо ничего не буду, для большинства java-программистов тут и так все ясно.
Вот собственно и все.
С помощью ListenerSupportFactory, имея только интерфейс слушателя IMyObjectListener, целевой observable-класс реализуется следующим образом:
слушателей самых различных видов и форм. Однако некоторые виды встречаются чаще других,
и ниже я приведу свой рецепт автоматизации их создания.
Возможно, предложенный мной подход не оригинален, но в литературе я его не встречал.
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-объекта необходимо:
- Хранить список слушателей (объектов типа IMyObjectListener) и управлять им, реализуя методы addMyObjectListener(IMyObjectListener) и removeMyObjectListener(IMyObjectListener)
- Для каждого метода, опред��ленного в интерфейсе слушателя, предусмотреть метод типа fireSomeEvent(...). Например, для поддержки слушателей типа IMyObjectListener придется реализовать три внутренних метода:
- fireDataAdded(MyObjectEvent event)
- fireDataRemoved(MyObjectEvent event)
- fireDataChanged(MyObjectEvent event)
- Вызывать методы 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); } }
