Я часто и много работаю со 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);
}
}