Когда разработчики начали использовать JavaScript и Node для более крупных и часто более сложных проектов, проблемы с обратными вызовами стали очевидными, и было предложено несколько альтернатив. В этом посте я расскажу о сочетании силы промисов и потоков функционального реактивного программирования в качестве парадигм асинхронного программирования.

Обещание учебника

Хотя для Node доступно несколько отличных библиотек промисов, я буду говорить о промисах ES6. Причина, по которой я предпочитаю их другим, заключается в том, что я верю, что в течение следующих 18–24 месяцев они станут предпочтительным обещанием на Node.

Основная предпосылка обещаний заключается в том, что вместо обратного вызова функция возвращает обещание. Возвращенное обещание может находиться ровно в одном из трех состояний: ожидание, выполнение или отклонение. Выполнено и отклонено — это «состояния выхода». Как только промис переходит в одно из этих состояний, дальнейшие переходы между состояниями не допускаются.

Как потребитель обещания, вы регистрируете функцию, которая будет выполняться, когда обещание выполнено, и, опционально, функцию, которая будет выполняться, если обещание отклонено.

app.get('/pendingOrders', function (req, res) {
    loadUserOrders(req.user).then(function (orders) {
        res.send(200, JSON.stringify(orders));
    }).catch(function (error) {
         res.send(500, JSON.stringify(error));
    });
});

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

Больше, чем простой случай

В сценарии в праймере обещания нам нужно было выполнить пару действий:

  • Загрузить заказы пользователя
  • Вернуть эти заказы пользователю

Обещания — отличный выбор для этих типов линейной деятельности; вещи, которые в синхронных языках ввода-вывода, таких как Go, были бы просто одной функцией, и где JavaScript добавляет, казалось бы, ненужную сложность в код.

Теперь давайте сделаем этот сценарий немного более сложным (и, возможно, немного более реальным):

  1. Получайте заказы пользователей с нашего сайта
  2. Получайте заказы пользователей из нашей внутренней системы продаж
  3. Определите, какие заказы были отправлены через UPS, а какие через USPS.
  4. Получите последнюю информацию об отслеживании заказов, отправленных через UPS
  5. Получите последние сведения об отслеживании заказов, отправленных через USPS
  6. Объедините все это и отправьте эту информацию обратно вызывающему абоненту.

В этом более сложном сценарии шаги 1 и 2, а также 4 и 5 не зависят друг от друга. Это именно тот тип сценария, когда промисы сами по себе не решают проблему. Вы можете заставить это работать с помощью только промисов, но это включает в себя много того же жонглирования, которого мы пытаемся избежать, не используя обратные вызовы (promise.all может быть полезен, но не без риска введения хрупкости и зависимости). Это также именно тот сценарий, в котором потоки FRP могут прийти на помощь и сосредоточить ваше внимание на добавлении ценности вместо подсчета ссылок на данные или беспокойстве о том, где расположить функции, чтобы они вызывались правильно.

В качестве потоковой структуры FRP мы будем использовать FRHTTP. Фреймворк FRHTTP относительно доступен и создан для Node (и на самом деле наиболее распространенная работа для Node — обслуживание данных).

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

Давайте посмотрим, как будет выглядеть более сложный сценарий в сочетании FRHTTP и промисов (справедливое предупреждение: чтобы не увязнуть, мы будем использовать множество воображаемых API для получения данных из разных систем):

Первое, на что следует обратить внимание, — это отдельные функции «когда». Каждая функция принимает следующие параметры:

  • имя (для целей отладки и сообщения об ошибках вы можете передать null, хотя лучше использовать разумное имя)
  • какие входные данные требуются вашей функции
  • какие выходные данные разрешено создавать вашей функции (не путайте это с типами данных, это список имен параметров, которые вы создаете).
  • функция, которая будет преобразовывать входы в выходы
  • любые дополнительные параметры. В этом примере используются takeMany и triggerOn. Установка для takeMany значения true означает, что каждый раз, когда производится любой из наших входных данных, эта функция вызывается снова. Установка значений для triggerOn фильтрует, какие данные вызовут повторное выполнение нашей функции.

Обратите внимание, что каждая функция when выполняет одну задачу и не заботится о том, откуда берутся данные или что происходит с данными, которые производит функция. Мы также не писали никакого «кода подключения». Давайте посмотрим, что происходит.

Мы начинаем со строки 4, извлекая userId из маршрута и создавая пустой массив shipOrders, который мы будем заполнять позже. Пустой массив необходим, потому что «когда» не вызывается до тех пор, пока не будут доступны все необходимые входные данные, поэтому, если мы не укажем пустую начальную точку, наша функция агрегации заказов (слияние) не будет вызвана.

Строки 9–14 и 15–20 получают заказы из двух наших отдельных систем. Они не заботятся друг о друге, только о вводе, который им требуется (userId).

В строках 21–31 все становится проще с FRHTTP и немного сложнее без него. Здесь мы собираем заказы и разделяем их на заказы, отправленные UPS и USPS. Опция takeMany сообщает системе, что каждый раз, когда кто-либо создает «заказы», ​​вызывайте эту функцию для ее преобразования.

Строки 32–39 и 40–47 содержат информацию о заказе от UPS и USPS соответственно. Мы снова используем takeMany, чтобы сделать «следующий шаг» для любого значения, которое мы принимаем в качестве входных данных. Нас не волнует, сколько раз любой из них вызывается, нас интересует только один шаг. Об остальном заботится система.

Строки 48–51 объединяют заказы (теперь с информацией о доставке) обратно в один набор. Мы снова используем takeMany для вызова в любое время при изменении одного из наших входных данных, но в этом случае мы также используем triggerOn. Поскольку нам нужен предыдущий массив всех заказов, и мы создаем этот массив, мы не хотим получать уведомления об его изменении (поскольку мы сами его меняем). Параметр triggerOn позволяет нам указать, что нам важно только то, когда доступен новый заказ с информацией о доставке, что позволяет нам игнорировать массив.

Строки 52–54 возвращают ответ пользователю.

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

Если в будущем нам понадобится внедрить какое-то новое поведение или дополнительное преобразование в маршрут, мы можем просто добавить новую функцию «когда» в конец списка функций «когда». Нет необходимости искать правильное место для внедрения поведения, новый блок будет просто вызван системой в нужное время на основе требуемых данных.

Как видите, промисы и FRHTTP составляют мощную пару, которая позволяет вам сосредоточиться на добавлении ценности, не беспокоясь о том, как перемещать данные.

Выучить больше

Если вам интересно узнать больше об обещаниях ES6, вы можете прочитать о них здесь.

Если вы хотите узнать больше о FRHTTP или использовать его в своем следующем или существующем проекте, вы можете узнать больше об этом на GitHub. Вы можете использовать FRHTTP как в автономном режиме, так и как часть приложения ExpressJS.

Обо мне

Я Амир Ясин; разработчик-полиглот, глубоко заинтересованный в высокой производительности, масштабируемости, архитектуре программного обеспечения и в целом решении сложных задач. Вы можете следить за мной на Medium, где я веду блог о программной инженерии, следить за мной в Twitter, где я иногда рассказываю интересные вещи, или проверять мой вклад в сообщество FOSS на GitHub.