DO notation
https://t.me/pepegrammingЧасто рассказываю о новой фиче 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
Минусы
Как и везде, минусов тоже хватает:
- Результатом выполнения кода, который используется с `yield`, должна быть монада, иногда выглядит громоздко;
- Если делать без контейнеров — заинжектить определенный шаг не получиться;
- Монады вызывают ужас у неподготовленных;
- Документации мало, не обкатано как транзакции, но проблем пока не возникало;
- Еще один способ сделать сервис объект;
Запомнить
- Dry-transaction перестает работать, когда между шагами много стейта и (или) нужно вызывать шаги условно;
- С версии 1.0 dry-monads предоставляют логику работы с шагами, которая может решить проблемы dry-transaction;
Ссылки
- PR с которого все началось;
- Railway programming;
- Предыдущие статьи про чейнинг данных, монады, транзакции;