ПромисПайпы: гомогенная бизнес-логика

Когда-то я был бэкенд-разработчиком, а до этого я занимался фронтендом. Я думаю, что я был неплохим бэкендом для моих фронтенд-коллег, так как я думал об API с точки зрения его использования.

Мне как фронтенд-разработчику везло меньше. Создание API — это сложно. Задача сформировать общее понимание того, каким должно быть взаимодействие клиента с сервером, забирает много времени и энергии. Мы начинаем с REST API, но у каждого свое понимание того, что такое REST, затем каждая модификация API требует усилий, времени и долгих обсуждений.

Сейчас, для описании запросов ресурсов с сервера, я использую Промисы. Мне они симпатичны, поскольку у них удобное «chaining API» для потенциально асинхронной бизнес-логики.

Я думаю, что любую бизнес-логику можно представить как цепочку преобразований данных. К примеру, сохранение объекта в базу данных — это преобразование данных в идентификатор объекта в базе данных.

Давай посмотрим код простой бизнес-логики, построенной на Промисах:

Promise.resolve(item)
  .then(validateItem)
  .then(postItem)
  .then(addItem)
  .catch(handleError)

postItem возвращает Промис, который будет выполнен, когда придет ответ с сервера.

На стороне сервера скорее всего у нас будет такой Node.js/Express код:

app.post('/api/items',
  validateItemMiddleware,
  saveItemInDBMiddleware,
  returnItemMiddleware)

Я думаю, что если бы Промисы появились раньше — @tjholowaychuk бы использовал их в Express.js вместо «middleware» функций.

На Промисах, сервер бы наверное выглядел как-то так:

app.post('/api/items')
  .then(validateItem)
  .then(saveItemInDB)
  .then(returnItem)

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

var postItem = function(data){
  return Promise.resolve(data)
    .then(validateItem)
    .then(saveItemInDB)
    .then(returnItem)
}

Promise.resolve(item)
  .then(validateItem)
  .then(postItem)
  .then(addItem)
  .catch(handleError)

Ясно-понятно, что в этом случае нам не нужно будет API, и мы не будем тратить время на обсуждение «как назвать путь» и «какой выбрать метод» в данном случае.

Если пойти еще дальше, то наш код можно сделать более плоским:

Promise.resolve(item)
  .then(validateItem)
  .then(validateItemServer)
  .then(saveItemInDB)
  .then(addItem)
  .catch(handleError)

Конечно, с простыми Промисами нам этого не сделать, но можно сделать с PromisePipe.

«PromisePipe» — это конструктор переиспользуемых цепочек Промисов. И, так как у ПромисПайпа больше контроля над выполнением цепочки, он дает возможность контролировать и модифицировать исполнение каждого звена.

ПромисПайп — это синглтон. Можно строить гомогенные цепочки бизнес-логки и запускать параллельно на клиенте и сервере. Звенья, помеченные как серверные, будут выполняться на сервере, а клиентские — на клиенте. В ПромисПайпе нужно имплементировать методы, которые будут прокидывать сообщения между клиентом и сервером. Причем, можно использовать любые транспорты, которые могут пересылать сообщения.

Схема

Итак, нужно будет написать немного кода, который будет прокидывать эти сообщения. Посмотреть примеры можно тут. Пока я в основном использовал socket.io как транспорт (http, udp, websocket и т.д.), но не должно быть проблем с простыми HTTP, или любым другим протоколом, который может пересылать сообщения.

Скринкаст

C ПромисПайп бизнес-логика будет выглядеть примерно так:

var doOnServer = PromisePipe.in('server')
var addItemAction = PromisePipe()
  .then(validateItem)
  .then(doOnServer(validateItemServer))
  .then(doOnServer(saveItemInDB))
  .then(addItem)
  .catch(handleError);
addItemAction(item) // will pass complete chain

Когда выполнение цепочки подходит к validateItemServer, ПромисПайп перекидывает исполнение на сервер с помощью специального сообщения. validateItemServer и saveItemInDB выполняются на сервере, и сообщение отсылается назад для продолжения выполнения на клиенте начиная с addItem.

В ПромисПайпе можно расширять API своими методами, что дает построить API так, что оно максимально удобно опишет бизнес-логику:

var doOnServer = PromisePipe.in('server')
var addItemAction = PromisePipe()
  .validate('item')
  .validateServer('item')
  .db.save.Item()
  .then(addItem)
  .catch(handleError);
addItemAction(item) // will pass complete chain

Например вот mongoDB API для ПромисПайп. А вот пример todo-app(live) приложения, которое использует «mongo-pipe-api». validateServer и «mongo-pipe-api» должны быть помечены как серверные методы. И тогда эти звенья будут выполняться на сервере.

С ПромисПайп можно строить бизнес-логику с помощью цепочек простых трансформаций, которые способны выполняться в разных процессах, в то время как логика остается простой и гомогенной.

С ПромисПайп мы получаем:

Схема

Я думаю, что ПромисПайп поможет в создании микросервисных архитектур. Гомогенная бизнес-логика позволяет отделить логику от межпроцессного взаимодействия, стирая разницу между кодом монолитной и микросервисной архитектуры.