Pull to refresh

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

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

Полезные источники:
Tags:
Hubs:
Total votes 21: ↑16 and ↓5+11
Comments7

Articles