Опять рассмотрим tasks#create
экшен.
В экшене 3 разных логики, которые выполняются последовательно:
- валидация данных - необходимый шаг;
- сохраниение таска - необходимый шаг, если какая-то ошибка, необходимо возвращать failed значение;
- отправка нотификаций - мы не хотим, что бы наша транзакия не выполнялась, если отправка нотификации не выполнится;
Поэтому напишем нашу транзакцию. Так же мы будем использовать Either монаду для возвращения статуса шага транзакции. Right
для успешного, Left
- не успешного:
require "dry/transaction"
class CreateTask
include Dry::Transaction
step :validate # первый шаг
try :persist # второй шаг
tee :notificate # третий шаг
def validate(params)
if paams.valid?
Right(parms.to_h) # параметры нужны для persist метода
else
Left(parms.to_h) # параметры нужны для создания энтити в экшене
end
end
def persist(params)
params[:body] = Markdown.parse(hash[:md_body])
params[:status] = Task::VALID_STATUSES[:in_progress]
params[:approved] = nil
Right(TaskRepository.new.create(params)) # если все хорошо - возвращаем task энтити для notificate шага
end
def notificate(task)
NewTaskNotificationWorker.perform_async(task.id)
end
end
Обновим контроллер:
module Web::Controllers::Tasks
class Create
include Web::Action
expose :task
params do
# ...
end
def call(params)
return unless authenticated?
result = CreateTask.new.call(params)
if result.success?
flash[:info] = INFO_MESSAGE
redirect_to routes.tasks_path
else
@task = Task.new(result.value)
self.body = Web::Views::Tasks::New.render(format: format, task: @task,
current_user: current_user, params: params, updated_csrf_token: set_csrf_token)
end
end
end
end
Экшен опять стал чище и вся лишняя логика теперь в транзакции. Давайте воспользуемся матчером, вместо лишнего условия:
module Web::Controllers::Tasks
class Create
include Web::Action
expose :task
params do
# ...
end
def call(params)
return unless authenticated?
CreateTask.new.call(params) do |m|
m.success do
flash[:info] = INFO_MESSAGE
redirect_to routes.tasks_path
end
m.failure do |task_params|
@task = Task.new(task_params)
self.body = Web::Views::Tasks::New.render(format: format, task: @task,
current_user: current_user, params: params, updated_csrf_token: set_csrf_token)
end
end
end
end
end
Вот и все. Что мы получили:
- больше не нужно переживать из-за сессии, что бы проверить создание таска в тестах;
- можно воспользоваться DI и протестировать экшен с
NullTransaction
который будет возвращать нужный результат без вызова бизнес логики и работы с BD; - убрав лишние методы в экшене, нужно думать, зачем нужен метод
task_params
и почему было именно так; - каждый из шагов транзакции можно вынести в отдельный класс, что бы изолированно протестировать и легко контролировать логику в этом классе;