Каркас для Event-Driven программирования

в 20:40, , рубрики: event-driven, java, Программирование, событийное программирование, теги никто не читает, метки: , , ,

Немного лирики

image
Чем дольше я программирую, тем больше мне нравятся слабосвязанные системы, которые состоят из большого числа разрозненных единиц (модулей), ничего не знающих друг о друге, но предполагающих существование других. Такие системы в идеале должны собираться подобно конструктору, без зависимостей и без адаптирования друг к другу. В идеале, в процессе работы такой системы, все необходимые задачи должны выполняться без остановки системы, простым введением нового модуля в мир (скажем, вбросом jar'ника в classpath), и система немедленно начнет взаимодействовать с новым модулем.
В связи с этим, очень привлекательно выглядит парадигма event-driven (или Событийно-ориентированное) программирование.

Его бесспорным плюсом является то, что не нужно править существующий код при добавлении или удалении какого-то модуля, система безостановочно продолжит работать, мы просто обретем или потеряем часть функциональности.
Надо оговориться, минусом является его же плюс — непредсказуемость поведения при изменениях, а также ослабление контроля над системой в целом (кто виноват, почему работает не так?) — в отличие от традиционного программирования, где система реагирует на подобные аномалии немедленно и жестко, с указанием причин (нужный модуль не найден).

Зачем менять традиции

В какой-то мере событийно-ориентированное программирование проявляется везде — от современных многозадачных ОС, до всяческих фреймворков. Вместо того, чтобы каждый участник (модуль) слушал все события, он подписывается только на те, какие ему интересны, тем самым потребляя меньше машинных ресурсов. Вообще-то, даже вызов метода на объекте можно воспринимать как синхронную посылку-прием сообщений с гарантией доставки и получения. Так зачем лишние сложности?
Ответ в том, чтобы все работало при любом исходе. Нас не интересует, кто получил наше сообщение, сколько получателей, и ответит ли он вообще. Мы просто мягко уведомляем. Никому не интересно — и ладно. Интересно — вот и отлично, работаем дальше.

Реалии

Например, та же событийная система в Java Swing. Чтобы слушать какие-то события, для каждого компонента есть методы addXXXListener, removeXXXListener. Для посылки таких сообщений есть метод fireXXXEvent. Все прекрасно. Но, скажем, мы в той же философии пишем свой компонент. И хотим посылать разнообразные события или реагировать на них, с сохранением инкапсуляции. Следовательно, приходится каждый раз реализовывать эти методы для каждого ХХХ события, для каждого компонента…

Решение

Код всегда до отвращения похожий, потому хотелось бы его заменить на несколько строчек. Подумал я, и в итоге не одного дня реализовал хелпер для таких задач. Это будет класс со статическими методами, который можно вызывать из любой точки программы. Итак, что нам нужно?
Во-первых, мы хотим реагировать на любые события. Пусть, для определенности, наши события будут реализовывать стандартный интерфейс java.util.EventObject, а слушатели будут реализовывать интерфейс java.util.EventListener. Так, с одной стороны, мы ничем не ограничены, а с другой — максимально просто связать это с событийной парадигмой AWTSwing. Тогда, пожалуй, подписка на событий должна выглядеть так:

public static synchronized void listen(final Class<? extends EventObject> event, final EventListener listener);

Наивная реализация выглядит так:

		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);
		}

Так мы выразили свое желание быть в курсе событий какого-то подкласса EventObject, пообещав, что реализуем интерфейс EventListener (он не определяет методов, об этом позже). Но нас точно уведомят, если конкретное событие произойдет.

Далее, для чистого выхода нам нужна способность отписаться от события, подойдет

public static synchronized void forget(final Class<? extends EventObject> event, final EventListener listener);

И тривиальный код:

	public static synchronized void forget(final Class<? extends EventObject> event, final EventListener listener) {
		if (Events.listeners.containsKey(event)) {
			Events.listeners.get(event).remove(listener);
		}
	}

А также для совсем чистого выхода (хотя здесь спорный момент):

public static synchronized <E extends EventObject> void forget(final Class<? extends EventObject> event);

И естественный код:

	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);
		}
	}

Хорош при выходе. Вроде бы и все. А, нет, не все — еще нужно однострочно же во многих местах уведомлять о каких-то тех или иных событиях, выглядит это так:

public static synchronized void fire(final EventObject event, final String method);

Код получился однопоточным:

	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);
				}
			}
		}
	}

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

Теперь, вместо реализации методов все может выглядить так, как в демо (для примера я выбрал 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);// слушаем событие
		//
		final ActionEvent event = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "command");
		Events.fire(event, "actionPerformed");// ActionListener.actionPerformed(ActionEvent)
		//
		Events.forget(ActionEvent.class); // очистка

Единственное неудобство в том, что в методе Events.fire необходимо указывать строкой еще и имя метода, и чтобы оно обязательно принимало один аргумент — объект нашего события. Так получилось потому, что разные слушатели реализовывают разные методы реакции на сообщения, и даже один слушатель может иметь несколько таких методов — соответственно типу события (Вроде как MouseListener определяет несколько методов, например, mouseOver и mouseOut, а также другие).
Ну и в заключение еще покаяние: все методы статично синхронизированы, надо заменить обычные коллекции на потокобезопасные. Еще более замедляет работу (в пересчет на наносекунды, в сравнении с прямым вызовом методов) рефлексия — которая происходит в цикле при возбуждении события для каждого слушателя, но, думаю, это необходимое зло.

Криво, неудобно и ненужно

Мне самому настолько понравился данный подход, что решил поделиться библиотекой с сообществом (здесь будет ссылка на гитхаб или что-то вроде). Если не нравится, ставьте минусы, а вообще буду рад конструктивной критике в комментах и дельным предложениям и дискуссиям.

Автор: Lure_of_Chaos

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js