Skip to content

Instantly share code, notes, and snippets.

@davydovanton
Last active February 21, 2022 08:51
Show Gist options
  • Save davydovanton/0a9e9dcaef75582e3c2fe9b4392b61d9 to your computer and use it in GitHub Desktop.
Save davydovanton/0a9e9dcaef75582e3c2fe9b4392b61d9 to your computer and use it in GitHub Desktop.

Пример рефакторинга с использованием транзакций

Опять рассмотрим tasks#create экшен.

В экшене 3 разных логики, которые выполняются последовательно:

  1. валидация данных - необходимый шаг;
  2. сохраниение таска - необходимый шаг, если какая-то ошибка, необходимо возвращать failed значение;
  3. отправка нотификаций - мы не хотим, что бы наша транзакия не выполнялась, если отправка нотификации не выполнится;

Поэтому напишем нашу транзакцию. Так же мы будем использовать 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

Вот и все. Что мы получили:

  1. больше не нужно переживать из-за сессии, что бы проверить создание таска в тестах;
  2. можно воспользоваться DI и протестировать экшен с NullTransaction который будет возвращать нужный результат без вызова бизнес логики и работы с BD;
  3. убрав лишние методы в экшене, нужно думать, зачем нужен метод task_params и почему было именно так;
  4. каждый из шагов транзакции можно вынести в отдельный класс, что бы изолированно протестировать и легко контролировать логику в этом классе;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment