DO notation

Часто рассказываю о новой фиче dry-monads — Do notation. Документации пока нет, но штука уже работает в проектах (повод сделать вклад в open source и помочь с документацией).

dry-transactions

Я уже писал о dry-transactions, это такая штука, которая благодаря steps реализует Railway Oriented Programming. Спустя год использования библиотеки нашлись проблемы.

1. Проброc стейта из шага в шаг. Сразу пример: сервис, который принимает account_id и вызывает шаги:

step :find_account
step :validate_account
step :get_orders
step :select_invalid_orders
step :persist

В таком случае из каждого шага приходится прокидывать +1 новый объект или менять существующие объекты:

def find_account(account_id:)
def validate_account(account:)
def get_orders(account:)
def select_invalid_orders(account:, orders:)
def persist(account:, orders:, invalid_orders:)

Когда шагов больше 3 — возникает путаница и аргументы раздуваются или появляются хеши с не понятным количеством аргументов

2. Нет условных переходов между шагами. Например, если я хочу создать запись или обновить — придется все держать в одном шаге:

step :create_or_update
def create_or_update(id:, payload:)
id ? repo.update(id, payload) : repo.create(payload)
end

3. Не критичные проблемы, такие как: шаги объявляются на уровне класса, а не метода (вкусовщина), методы нельзя сделать приватными. Кроме того, часто вижу вопрос как обернуть пару шагов в одну DB транзакцию (Around steps) выручает, но выглядит страшно)

Что с этим делать

В последнем (1.0.0.pre) релизе dry-monads появилась эмуляция Do нотаций хаскеля.

Эти изменения создают “шаги” выполнения логики в методе, который будет указан. Для этого инклюдим Dry::Monads::Do.for(:your_method_name) и миксины нужных монад в сервис. После этого, в методе вызываем yield с аргументом монадой. yield MonadInstance вернет либо значение “успешной” монады (Success, Some, etc), либо метод досрочно завершится и вернется Failure значение как это сделано в dry-transactions.

Полезности

Кроме преимуществ dry-transactions закрываются недостатки подхода с steps.

Во первых, можно не пробрасывать полный контекст из метода в метод, а пробрасывать только нужные объекты:

def call(account_id)
account = yield find_account(account_id)
yield validate_account(account)
orders = yield get_orders(account)
invalid_orders = yield select_invalid_orders(orders)
persist(account, orders, invalid_orders)
end

Во вторых, никто не запрещает использовать условный переход:

def call(id, payload)
payload = yield validate(payload)
  if id
create_object(payload)
else
update_object(id, payload)
end
end

Приватные методы работают без проблем. Обертка в транзакцию выглядит не так монструозно:

def call(id, payload)
# …
repo.transaction do
yield create_object(payload)
create_other(payload)
end
end

Минусы

Как и везде, минусов тоже хватает:

  1. Результатом выполнения кода, который используется с `yield`, должна быть монада, иногда выглядит громоздко;
  2. Если делать без контейнеров — заинжектить определенный шаг не получиться;
  3. Монады вызывают ужас у неподготовленных;
  4. Документации мало, не обкатано как транзакции, но проблем пока не возникало;
  5. Еще один способ сделать сервис объект;

Запомнить

  • Dry-transaction перестает работать, когда между шагами много стейта и (или) нужно вызывать шаги условно;
  • С версии 1.0 dry-monads предоставляют логику работы с шагами, которая может решить проблемы dry-transaction;

Ссылки