Refactoring Fat Model
Bài đăng này đã không được cập nhật trong 7 năm
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