0

Refactoring Fat Model

In the Ruby on Rails project it is a common practice to keep controller as small as possible and by doing that we push all the logic into model. Eventually as the application grow the model class became litter with code that has nothing to do with data persistence at all. This result in slow and hard to test code because creating a model object is so expensive.

To solve this problem we can apply OO-based principles and best practices into our Rails application by breaking thing up into self contains and manageable classes.

Don’t Extract Mixins from Fat Models

Your model class might look clean and organize on the surface but all you did was moved the code from one place to another and in this case it is the module that live inside concerns directory. Your internal structure of your model still the same.

Use Value Objects

Value Objects are simple objects whose equality is dependent on their value rather than an identity and they are usually immutable. In Rails attributes that needed more that text handle and counter which needed some logic to perform on them are prime candidates for value objects. Example class to represent Money, Rating, Address ...etc.

Suppose we have a User model that has city, state and other attributes. We can extract it into the following Address class

class Address
  attr_accessor :city, :state

  def initialize(city, state)
    @city, @state = city, state
  end

  def ==(other)
    city == other.city && state == other.state
  end
end
class User < ActiveRecord::Base
  def address
    @address ||= Address.new(city, state)
  end

  def address=(new_address)
    self.city = new_address.city
    self.state = new_address.state
    @address = new_address
  end
end

Extract Service Objects

Service objects are used to encapulate complex operation that is not a concern of the underlying model like interact with external service or operation that react out across multiple models or operation that has multiple ways of doing thing. Take for example, we can extract user authentication into UserAuthenticator class like this.

class UserAuthenticator
  def initialize(email, password)
    @email, @password = email, password
  end

  def authenticate
    user = User.find_by(email: @email)
    user && user.hash_password == encrypt_password(@password) && user.active? ? user : nil
  end
end

Extract Form Objects

When multiple ActiveRecord models might be updated by a single form submission, a Form Object can encapsulate the aggregation. Using form object is far more simpler and easier to read than using accepts_nested_attributes_for. A good example is user registration form which create both user and profile.

class Registration
  include ActiveModel::Model

  attr_accessor (
    :email,
    :username,
    :password,
    :password_confirmation,
    :first_name,
    :last_name,
    :language
  )

  validates :email, email: true, presence: true
  validates :username, presence: true
  validates :password, presence: true, confirmation: true
  validates :first_name, presence: true
  validates :last_name, presence: true

  # Never persisted a form object
  def persisted?
    false
  end

  def save
    if valid?
      user = User.create!(
        email: email,
        username: username,
        hash_password: encrypt_password(password)
      )

      user.profile.create!(
        first_name: first_name,
        last_name: last_name,
        language: language || 'en'
      )

      true
    else
      false
    end
  end
end

Notice that we include ActiveModel::Model this allow the form object to quack like ActiveRecord object. This means we can make use of the validation callback that we are already familiar with. Also in the view we can also use this form object with form_for helper much like we do with ActiveRecord object. For more advance usage of form object you can check out the reform gem.

Make Use of Decorators

The common thing to do in Rails application is probably the use of helper methods to format model object and display it in view. While there is nothing wrong with this approach it feels more of a procedural programming while we are working with OO environment. What I would like to do is creating a class to handle all this view logic while still keeping the exact same functionality like ActiveRecord object by using decorator. For example you might want to display a user full name base on user preference language.

class UserDecorator < SimpleDelegator
  def initialize(base, view_context)
    super(base)
    @object = base
    @view_context = view_context
  end

  def full_name
    profile = @object.profile
    if profile.language == 'ja'
      "#{profile.last_name} #{profile.first_name}"
    else
      "#{profile.first_name} #{profile.last_name}"
    end
  end

  def _h
    @view_context
  end
end

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí