ПромисПайпы: гомогенная бизнес-логика
Когда-то я был бэкенд-разработчиком, а до этого я занимался фронтендом. Я думаю, что я был неплохим бэкендом для моих фронтенд-коллег, так как я думал об 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» должны быть помечены как серверные методы. И тогда эти звенья будут выполняться на сервере.
С ПромисПайп можно строить бизнес-логику с помощью цепочек простых трансформаций, которые способны выполняться в разных процессах, в то время как логика остается простой и гомогенной.
С ПромисПайп мы получаем:
-
простоту
Строим логику в функциональном стиле используя простые трансформации данных. Забываем о межпроцессных взаимодействиях и концентрируемся на бизнес-логике.
-
тестируемость
Каждое звено можем тестировать отдельно. Так же просто склеиваем звенья в цепочки и тестируем целые куски функциональности изолированно.
-
изоморфизм
ПромисПайп созданы для работы в межпроцессных средах. Значит, мы получаем изоморфную бизнес-логику из коробки, если создаем звенья изоморфными. Так, если не использовать специфичного для среды API, то можно запускать пайп как в одном процессе, так и ожидать, что цепочка будет работать в среде браузер + сервер.
-
масштабирование
Каждое звено может запускаться в своем отдельном процессе без больших усилий. Это пока не значит, что мы получаем масштабируемость из коробки, но значит, что у нас есть довольно простой метод распределения нагрузки между многими процессами.
-
фронтендеры могу полностью описывать бизнес-логику
Цепочки и звенья легко стыкуются друг с другом. Основная идея — позволить фронтенд-разработчику, используя простые функциональные кирпичики, строить бэкенд, инкапсулируя сложность внутри осмысленных звений бизнес-логики.
Я думаю, что ПромисПайп поможет в создании микросервисных архитектур. Гомогенная бизнес-логика позволяет отделить логику от межпроцессного взаимодействия, стирая разницу между кодом монолитной и микросервисной архитектуры.