Каждый программист, начинающий разрабатывать под Node.js, встаёт перед выбором стратегии организации асинхронного кода в проекте. В то время, как в небольших системных утилитах поддерживать гигиену асинхронного кода достаточно просто, при росте массы кода в проекте решение этой задачи начинает требовать введения дополнительного, так называемого control flow средства.
В этой статье будет рассмотрена небольшая 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
Автор: geeqie