Amerov blog

Developer notes

Sep 14, 2016 - 2 minute read - Comments

ActiveRecord callbacks – Зло

Это спорное утверждение, поскольку использование ActiveRecord коллбеков довольно распространено. На самом деле, большинство из них можно выпилить, чтобы улучшить поддерживаемость проекта.

Основная проблема в том, что код с коллбеками становиться не прямолинейным. При использовании коллбеков приходится искать точки их вызова, перемещаться к определениям и обратно, разбираться в порядке вызовов… К тому же, они могу быть условными, что ёщё сильнее затрудняет поддерживаемость.

Еще одна возникающая трудность - коллбеки затрудняют рефакторинг моделей. Например, если вы захотите выделить бизнес логику в ServiceObjects.

Давайте посмотрим на примерах, как мы можем их выпилить.
Типичный пример модели:

# before_remove_callbacks_model.rb

class Customer < ActiveRecord::Base
  after_create :send_welcome_email, unless: :auto_registered?
  has_one :auto_created, dependent: :destroy

  private

  def send_welcome_email
    CustomerMailer.welcome_email(self).deliver
  end

  def auto_registered?
    !auto_created.nil?
  end
end

Пример контроллера:

# before_remove_callbacks_controller.rb

class CustomersController < ApplicationController
    # …
  def create
    @customer = Customer.new(customer_params)
    respond_to do |format|
      if @customer.save
        format.html { redirect_to @customer, notice: 'Customer was successfully created.' }
        format.json { render :show, status: :created, location: @customer }
      else
        format.html { render :new }
        format.json { render json: @customer.errors, status: :unprocessable_entity }
      end
    end
  end
    # …
end

Здесь от нас скрыто, что при сохранении Customer без auto_created происходит отправка письма.

Решение получше – избавиться от коллбеков и добавить два метода класса – register и auto_create:

#after_removing_callbacks_model.rb

class Customer < ActiveRecord::Base
  has_one :auto_created, dependent: :destroy

  def self.register(params)
    new(params).tap do |customer|
      customer.save!
      customer.send_welcome_email
    end
  end

  def self.auto_create(params)
    new(params).tap do |customer|
      customer.save!
    end
  end

  def send_welcome_email
    CustomerMailer.welcome_email(self).deliver
  end
end

Был убран метод auto_registered? и условия связанные с ним.
Код модели стал проще.

Пример контроллера после изменений в модели:

# after_removing_callbacks_controller.rb

class CustomersController < ApplicationController
# …
  def create
    if auto_create_request?
      @customer = Customer.auto_create(customer_params)
    else
      @customer = Customer.register(customer_params)
    end

    respond_to do |format|
      format.html { redirect_to @customer, notice: 'Customer was successfully created.' }
      format.json { render :show, status: :created, location: @customer }
    end

  rescue ActiveRecord::RecordInvalid
    respond_to do |format|
      format.html { render :new }
      format.json { render json: @customer.errors, status: :unprocessable_entity }
    end
  end

  def auto_create_request?
    customer_params[:auto_created].present?
  end
    # …
end

Условная логика перемещается вверх до уровня приложения. Это оптимальное решение, поскольку мы перемещаем логику прикладного уровня к естественной границе.В Rails этой естественной границей является HTTP-протокол и контроллер.

Это условие может быть полностью устранено путем добавления дополнительных экшенов #register и #auto_create.

Таким образом, логика ветвления находится только на верхнем уровне кода, и поток полностью линейный. Еще лучше, если извлечь всё это в ServiceObject и перенести туда логику отправки письма - таким образом, еще эффективнее упрощая этот код и логику контроллера.


Оригинал -> https://medium.com/planet-arkency/the-biggest-rails-code-smell-you-should-avoid-to-keep-your-app-healthy-a61fd75ab2d3

Tags: Ruby Rails Refactoring

comments powered by Disqus