Хабрахабр

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Например:

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

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

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

EV('погода', );

Где 'погода' — идентификатор сообщения, а {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-запроса.
И так далее. Хм… Параллельные ajax-start и ajax-finish могут приводить к неконсистентности индикатора.

Примечания

  1. В принципе любой jQuery триггер представляет собой шину событий. Свою реализацию EV вы могли бы сделать, используя триггер на произвольном фиксированном теге.
  2. Когда приложение становится сложным, то шина наполняется множеством событий. Если события пишутся разными разработчиками, то они вполне могут нечаянно пересечься в названиях. Решением указанной проблемы может быть простое полиси "выбираем идентификаторы так-то". Например, для работы с погодой уточняющие события мы называли с префиксом погода-. Хорошим вариантом будет так же дать возможность использования составного ключа в функции EV:

// эквивалентны:
EV(['погода', 'температура'], 10);
EV('погода-температура', 10);

Итоги

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

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть