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.