В этой статье будет рассмотрена небольшая control flow библиотека «Flowy», являющаяся развитием идей проекта Step Тима Касвелла, и ядро которой базируется на концепциях CommonJS Promises, а также приведены аргументы, почему же Promises — это так неудобно.
Как это выглядит
function leaveMessage(username, text, callback) { Flowy( function() { // concurrent execution of two queries model.users.findOne(username, this.slot()); model.settings.findOne(username, this.slot()); }, function(err, user, settings) { // error propagating if (!user) throw new Error('user not found'); if (!settings.canReceiveMessages) throw new Error('violating privacy settings'); model.messages.create(user, text, this.slot()); }, function(err, message) { model.notifications.create(message, this.slot()); }, callback //any error will be automatically propagated to this point ); }
- Каждый шаг, переданный в обертку
Flowy
выполняется в контексте библиотеки (переменнаяthis
). При этом контекст предоставляет возможность передавать данные на следующий шаг путем генерирования колбэков, которые можно передать классическим nodejs-like функциям в качестве последнего аргумента (вызовthis.slot()
). - Все, что выполняется в одном шаге, — выполняется параллельно.
- Управление будет передано следующему шагу лишь после того, как все его «слоты» будут заполнены данными — все колбэки, сгенерированные вызовом
this.slot()
завершатся успешно, либо же первый из них получит сообщение об ошибке. - При возникновении ошибки в любом из шагов выполнение всей цепочки будет прервано и ошибка будет возвращена в последний шаг.
Почему это выглядит именно так?
Программисту, начинающему знакомство с API неблокирующей подсистемы ввода-вывода Node.js, предлагается интерфейс асинхронных вызовов следующего вида:
fs.readFile('/etc/passwd', 'utf8', function (err, data) { if (err) throw err; console.log(data); });
При использовании чужих модулей естесственным желанием было бы иметь интерфейс, схожий с описанным выше — правило наименьшего удивления является одним из залогов поддерживаемого и легкоотлаживаемого кода. Отсюда появляется первое требование к библиотеке:
Мы хотим сохранить «родные» nodejs-like интерфейсы функций и колбэков. Каждый шаг Flowy
имеет интерфейс nodejs-колбэка, что позволяет легко оборачивать всю цепочку шагов в традиционную nodejs-функцию.
При этом, основной идеей Promises (в качестве примера реализации в дальнейшем будет использоваться библиотека «Q» Криса Коуэла) является замена передачи колбэка последним аргументом в асинхронный вызов созданием цепочки вызовов методов Promise:
// chaining promises: Q.fcall(step1).then(step2).then(step3).done() return getUsername() .then(function (username) { return getUser(username) .then(function (user) { // if we get here without an error, the value returned here // or the exception thrown here resolves the promise returned by the first line }) })
Первое, что бросается в глаза: функции возвращают Promise. Таким образом, для использования библиотеки необходимо все «классические» функции обернуть в Promise-адаптер (подробнее этот процесс описан на станице проекта), либо же разрабатывать код с жестко ориентированными на библиотеку интерфейсами (но при этом все публичные интерфейсы модуля необходимо будет обратно привести в классический вид, учитывая требование, сформулированное выше). Это неудобно. Это звучит пугающе и не менее пугающе выглядит. При этом сразу же на ум приходит второе требование к control flow библиотеке:
Библиотека должна быть лишь «клеем» между существующими частями системы и не становиться тяжелой зависимостью. Все особенности функционирования «Flowy» скрыты внутри шагов — того самого клея, — что позволяет функциям, использующим ее, оставаться «чистыми» для внешнего мира. Сор должен оставаться в избе.
При работе с библиотеками, позволяющими создавать цепочки (chaining) из асинхронных вызовов, часто возникает необходимость выполнить часть вызовов параллельно. Библиотека «Q» предоставляет следующее неловкое решение:
Q.allResolved(promises) .then(function (promises) { promises.forEach(function (promise) { if (promise.isFulfilled()) { var value = promise.valueOf(); } else { var exception = promise.valueOf().exception; } }) })
В добавок ко всему, если мы вдруг захотим нарушить правило «один аргумент — одно возвращенное значение», то придется заниматься дополнительными упражнениями:
return getUsername() .then(function (username) { return [username, getUser(username)]; }) .spread(function (username, user) { })
Читая этот код, само собой напрашивается еще одно требование к библиотеке:
Мы хотим легко выполнять несколько параллельных запросов и передавать любое количество аргументов в колбэки. «Flowy» это умеет без каких-либо дополнительных усилий со стороны разработчика благодаря своей архитектуре.
Итак, «Flowy» — это легковесная библиотека по управлению асинхронным потоком выполнения программы, позволяющая легко решать повседневные вопросы разработчиков под Node.js и хорошо зарекомендовавшая себя в production-окружении.
Данная статья демонстрирует лишь базовые возможности «Flowy». Для более подробного ознакомления, приглашаю всех посетить страничку проекта на гитхабе, где вы найдете обильную документацию со множеством примеров.
Полезные источники:
- Flowy: https://github.com/geeqie/node-flowy
- CommonJS: http://www.commonjs.org
- Q: https://github.com/kriskowal/q
- Обзорная статья про аналогичные библиотеки: http://habrahabr.ru/post/111634
ссылка на оригинал статьи http://habrahabr.ru/post/166419/
Добавить комментарий