Refactoring fat models in Rails: delegate extract and compose


Introduction

Reduce complexity in Rails models with the method delegation, extraction, and composition.

Initial Fat Model Example

Added comments to explain how we could group these 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

This model is overwhelmingly cumbersome and violates the Single Responsibility Principle by handling multiple aspects of the order process.

Refactoring using delegate Method

Firstly, we can extract the conversion methods into a separate class and then use Rails’ delegation mechanism to keep the interface of the Order model clean.

Creating the New OrderConverter Class

The responsibility of this class would be to convert the model into XML, JSON, CSV, and PDF, as the initial model was doing.

# 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

Updated Order model with Delegation

Let’s use the delegate method to orchestrate the creation of all methods in our Order.rb 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

With this setup, calls like @order.to_pdf seamlessly forward to OrderConverter, adhering to both the Single Responsibility and Law of Demeter principles.

Extracting Logic into a Separate Class

Another common refactor is moving related functionality into service classes, which can then be composed back into the main model.

Example: A Complex BankAccount Model

# 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 improperly handles diverse functionalities.

Using composed_of for Money Management

By introducing a Money class and linking it via composed_of, BankAccount responsibilities are clearly divided.

# 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

Controller Usage

@bank_account.balance.in_currency(:eur)

This keeps the BankAccount model lean and delegates monetary calculations to the Money class.

Likewise, you can compare the balances by using @bank_account.balance > @other_bank_account.balance, regardless of which currencies the bank accounts are using.

Conclusion

Refactoring fat models in Ruby on Rails through meticulous extraction and composition leads to cleaner, more maintainable codebases.

This approach not only aligns with SOLID principles but also enhances the flexibility and robustness of the application architecture.


Related posts

Free Training By Email:
Building a Rails Engine

We will send udpates but we care about your data.
No spams.

Back to homepage