RL ROLAND LOPEZ
// 1 min read

Refactoring Models in Rails

The problem

You cut model complexity with three moves: delegate, extract, and compose.

Here is a fat model, with comments grouping the methods:

# app/models/order.rb
class Order < ActiveRecord::Base
  # Various state finders
  def find_purchased
  end

  def find_waiting_for_review
  end

  def find_waiting_for_sign_off
  end

  # Search methods
  def advanced_search(fields, options = {})
  end

  def simple_search(terms)
  end

  # Export methods
  def to_xml
  end

  def to_json
  end

  def to_csv
  end

  def to_pdf
  end
end

It does too much: state finders, search, and four export formats. Classic SRP violation.

Refactor with delegate

Pull the conversion methods into their own class, then delegate so Order’s interface stays clean.

The new class

# app/models/order_converter.rb
# you can also move this to /lib or /concerns
class OrderConverter
  attr_reader :order

  def initialize(order)
    @order = order
  end

  def to_xml
  end

  def to_json
  end

  def to_csv
  end

  def to_pdf
  end
end

The model

# app/models/order.rb
class Order < ActiveRecord::Base
  delegate :to_xml, :to_json, :to_csv, :to_pdf, to: :converter

  def converter
    OrderConverter.new(self)
  end
end

Now @order.to_pdf forwards to OrderConverter. That respects both the Single Responsibility Principle and the Law of Demeter.

Extract, then compose

Another common move: pull related logic into a value class and compose it back in.

A complex BankAccount

# app/models/bank_account.rb
class BankAccount < ActiveRecord::Base
  validates :balance_in_cents, presence: true
  validates :currency, presence: true

  def balance_in_other_currency(currency)
    # Logic to convert in other currencies
  end

  def balance
    balance_in_cents / 100
  end

  def balance_equal?(other_bank_account)
    balance_in_cents == other_bank_account.balance_in_other_currency(currency)
  end
end

This model juggles too many responsibilities.

composed_of for money

Introduce a Money class and wire it with composed_of, so responsibilities split cleanly.

# app/models/bank_account.rb
class BankAccount < ActiveRecord::Base
  validates :balance_in_cents, presence: true
  validates :currency, presence: true

  composed_of :balance,
            class_name: "Money",
            mapping: [%w(balance_in_cents amount_in_cents),
                      %w(currency currency)]
end
# app/models/money.rb
class Money
  include Comparable
  attr_accessor :amount_in_cents, :currency

  def initialize(amount_in_cents, currency)
    self.amount_in_cents = amount_in_cents
    self.currency = currency
  end

  def in_currency(other_currency)
    # Logic to convert in other currencies
  end

  def amount
    amount_in_cents / 100
  end

  def <=>(other_money)
    amount_in_cents <=> other_money.in_currency(currency).amount_in_cents
  end
end

In a controller:

@bank_account.balance.in_currency(:eur)

BankAccount stays lean and Money owns the math. You can even compare across currencies with @bank_account.balance > @other_bank_account.balance.

Takeaway

Extraction and composition keep ActiveRecord models lean and SRP-aligned. You trade one bloated class for a few focused ones, and the app gets easier to change.

Roland Lopez
Written by
Roland Lopez

Technical founder & AI crack-head

Built by Agent Skynet See the agency