Именованные события: программируем GUI

— Вы заметили, сэры, какие стоят погоды?
— Предсказанные, — сказал Роман.
— Именно, сэр Ойра-Ойра! Именно предсказанные!
(Понедельник начинается в субботу)

В настоящее время большинство сайтов представляют собой, по сути, обычную программу, которая обрабатывает те или иные действия пользователей. Для того, чтобы упрощать жизнь программистам, реализованы десятки фреймворков, помогающих решать те или иные задачи.

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

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

О программировании слабосвязанных вещей в вебе мы и поговорим в этой статье.

Сразу обращаю внимание, что данная статья ставит целью описать подход к решению задачи, поэтому некоторая часть кода, приведённого в статье, может выглядеть “велосипедно”, и вы можете указать ссылку на более профессиональную реализацию.

Итак, поскольку мы с самого начала статьи сформулировали термин “слабосвязанный”, то логично будет описать уровень связи в разных подходах.

Отвлечёмся от программирования.

Вам необходимо уведомить вашего знакомого о том, какая нынче стоит погода. У вас есть множество путей. Выберем основные из них и расположим их в порядке ослабления взаимосвязи. Вы можете:

  1. Позвонить знакомому и сообщить ему нужную информацию.
  2. Отправить ему сообщение электронной почтой.
  3. Написать о погоде в блоге, соцсети. Кому надо - прочитает :)

Если вернуться к программированию, то перечисленное будет представлять собой:

  1. Обычный ООП/функциональный стиль: вызов метода.
  2. Отправка сообщения от объекта к объекту.
  3. Отправка широковещательного сообщения.

Поскольку третий вариант представляет собой способ, содержащий в себе минимум связности, рассмотрим его.

Однако сперва еще один экскурс-отвлечение. Если мы обратимся из мира веб-программирования в мир обычного GUI (например, операционные системы), то увидим, что там сталкиваются с тем же набором проблем.

В каждой операционной системе существует решение, позволяющее связывать между собой слабосвязанные вещи. В Linux этим решением является dBus.

Как это работает? Когда, например, пользователь закрывает крышку ноутбука, или когда сетевой интерфейс производит подключение к внешнему миру, то в шину dBus отправляется сообщение.

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

  • (драйвер крышки) если кому-то интересно, то крышка ноутбука открыта!
  • (сетевая карта) соединение с внешним миром установлено!
  • (плеер) я здесь показываю фильм пользователю!
  • (датчик температуры) у меня 69 градусов!
  • (драйвер USB) у меня тут флешку воткнули!
  • (плеер) я все еще показываю фильм пользователю!

Несколько утрировано и несколько оторвано от реальности, но наглядно :)

Теперь, если мы хотим понимать, что происходит в системе, мы просто слушаем системную шину. А если хотим интегрироваться с системой, то начинаем и отправлять данные в неё.

Вернемся к вебпрограммированию и попробуем реализовать аналогичную шину для сайта.

Прежде всего, определимся с сообщениями. Сообщение имеет тип (идентификатор) и произвольные данные, связанные с сообщением.

Например:

  1. Тип сообщения - “погода”
  2. Связанные данные - температура, давление, ветер, осадки, и т.п.

Но давайте прекратим рассуждать и начнем программировать. Выберем простой API. Мне нравятся короткие идентификаторы. Зарезервируем себе короткое имя в глобальном неймспейсе. Например, EV.

Отправлять сообщения будем, например, так:

EV('погода', { t: 10 });

Где ‘погода’ - идентификатор сообщения, а {t: 10} - сопуствующие данные (температура).

Теперь в коде, который принимает погоду по AJAX, просто впишем после успешного ее приема:

$.ajax({
   url: 'http://погода-по-ajax.bla',
   success: function(data) {
	// прочий код
	EV('погода', data);
   }
});

Все, на этом интеграция источника данных с внешним миром завершена.

Заинтересованный в погоде код может анализировать и дробить сообщения на более точные:

EV.on('погода', function(data) {
	if ('t' in data) {
		EV('погода-температура', data.t);
	}
	if ('wind' in data) {
		EV('погода-ветер', data.wind);
	}
});

Как видим, принявший данные код тоже может отправлять события.

При дроблении событий на более мелкие мы сразу сталкиваемся с тем, что идентификаторы событий также требуют рефлексии над их неймспейсами. Обсудим это позднее.

Далее, если у нас, например, есть уголок, где надо показать температуру, то используем обработчик, “слушающий” шину и выводящий результат в нужное место на сайте:

EV.on('погода-температура', function(t) {
	$('#my-temp').text(t);
	$('#my-temp').removeClass('hidden');
});

API шины, что мы придумали, состоит из двух методов:

  • отправки сообщения в шину EV();
  • подписки на сообщения шины EV.on() (фильтруя их на точные соответствия).

Просто, не правда ли? Блок, получающий погоду, ничего не знает о блоке отправляющем. Если отправляющий вдруг сломается, это приведет лишь к тому, что данных принимающий не получит.

Если принимающий удалится или, напротив, размножится, то, опять же, это никак не повлияет на отправляющего.

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

Библиотека EV может выглядеть примерно так:

window.EV = function() {
	var name = arguments[0];
	if (name in window.EV._list) {
		var args = [];
		for (var i = 1; i < arguments.length; i++)
			args.push(arguments[i]);
		for (var i in s[name]) {
			try {
				window.EV._list[name][i].apply(null, args);
			} catch(e) {
				console.log(e);
			}
		}
	}
};

window.EV._list = {};		// подписчики

window.EV.on = function(name, cb) {
	if (!(name in window.EV._list))
		window.EV._list[name] = [];
	window.EV._list[name].push(cb)
};

Обработку исключений в обработчиках можно исправить по вкусу.

Взяв на борт сайта подобную библиотеку, обнаруживаем, что, помимо программирования связности, некоторые сложные ранее проблемы становятся более простыми.

Например, реакция на клики, события, изменения по элементам, пришедшим по AJAX:

$('body').on('click', '[data-event-click]', function() {
	EV($(this).attr('data-event-click'), $(this).attr('data-event-arg'), $(this));
});

$('body').on('change', '[data-event-change]', function() {
	EV($(this).attr('data-event-change'), $(this).attr('data-event-arg'), $(this));
});

Оформили подобный код однажды в библиотеку и навсегда забыли теги onclick или биндинги $(selector).on().

Теперь, если нам нужна кнопка, нажатие по которой посылает в нашу системную шину событие, то биндиться к ней не нужно:

<button  data-event-click="кнопка" data-event-arg="красная">
	Текст на кнопке
</button>

Реакция на изменение селекта может выглядеть так:

<select name="bla" data-event-change="Выбор стиля">
	<option>Красный</option>
	<option>Зеленый</option>
</select>

Подобный подход хорошо себя показал в разработке одностраничных многоэкранных приложений (когда весь код wizard’а грузится сразу в скрытые блоки и пользователь путешествует по лабиринтам диалогов без перезагрузки всей страницы).

Задействовав подобную библиотеку, вы вскоре обнаружите, что она начнет быстро обрастать плагинами. Один (click/change event) мы уже написали.

Давайте напишем второй. Предположим, нам надо выждать паузу и что-то сделать.

window.setInterval(function() { EV('system-clock') }, 250);

Теперь шина наполняется периодическими событиями. С точки зрения расхода CPU/батарей, может быть, не сильно хорошая мысль делать такой плагин, однако с точки зрения простоты программирования некоторых вещей - вполне.

Теперь, подписавшись на это событие, мы можем, скажем, менять цвет/размер кнопки, побуждая пользователя нажать ее. Либо выполнять периодические AJAX запросы за какими-то данными. Либо выводить/обновлять часы в одном из блоков сайта.

Еще пример. Если пропатчить вашу ajax библиотеку, чтобы перед запросом она слала в системную шину сообщение ajax-start, а по завершении - ajax-finish, то в любом месте можем повесить красивый индикатор:

EV.on('ajax-start', function() {
	$('#wifi-icon').addClass('highlight');
});
EV.on('ajax-finish', function() {
	$('#wifi-icon').removeClass('highlight');
});

Хм… Параллельные ajax-start и ajax-finish могут приводить к неконсистентности индикатора. Значит, патч еще должен отправлять порядковый номер AJAX-запроса. И так далее.

Примечания

  1. В принципе любой jQuery триггер представляет собой шину событий. Свою реализацию EV вы могли бы сделать, используя триггер на произвольном фиксированном теге.
  2. Когда приложение становится сложным, то шина наполняется множеством событий. Если события пишутся разными разработчиками, то они вполне могут нечаянно пересечься в названиях. Решением указанной проблемы может быть простое полиси “выбираем идентификаторы так-то”. Например, для работы с погодой уточняющие события мы называли с префиксом погода-. Хорошим вариантом будет так же дать возможность использования составного ключа в функции EV:
// эквивалентны:
EV(['погода', 'температура'], 10);
EV('погода-температура', 10);

Итоги

Используя “системную шину” событий (именованные события), можно писать устойчивые и сложные приложения, разделяя код на практически полностью независимые друг от друга блоки. От того, что вы временно удалите тот или иной блок, перестанут работать только зависимые от него блоки. Система в целом будет устойчивой.

Разные части приложения могут писаться программистами, которые могут и не общаться друг с другом.