Как стать автором
Поиск
Написать публикацию
Обновить

Не надо давать обещания, или Promises наоборот

Время на прочтение4 мин
Количество просмотров9.5K
Каждый программист, начинающий разрабатывать под 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». Для более подробного ознакомления, приглашаю всех посетить страничку проекта на гитхабе, где вы найдете обильную документацию со множеством примеров.

Полезные источники:
Теги:
Хабы:
Всего голосов 21: ↑16 и ↓5+11
Комментарии7

Публикации

Ближайшие события