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 ...
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