Sep 27, 2017 9:00:46 PM | Even More Tips for Refactoring Rails Models

Even more tips for refactoring rails models, with code to help you ensure your Rails application is as slim and robust as it can possibly be.

There are many indicators that your models have grown too big, but the most indicative is when your model breaks the single responsibility principle. First introduced by software engineer and author Robert Cecil Martin in his book Agile Software Development, Principles, Patterns, and Practices, the single responsibility principle states that every class in an application should have responsibility over one, and only one aspect of the overall software. Martin defines "responsibility" as a "reason to change," indicating that each class should only ever have one reason to change.

In our Top Tips for Refactoring Fat Models in Rails article we looked at a handful of suggestions for cleaning up and streamlining bloated models. Today, we'll be looking at even more tips for refactoring Rails models, so let's jump right in!

Create View Objects

In many cases, your Rails application may need to perform some logic to determine what views and other UI elements are active for the user. However, while it's relatively easy to start throwing such logic directly into .erb files, refactoring this logic into view objects can save a lot of time and headaches down the road.

For example, consider a scenario where our application needs to present a series of navigation menu links (i.e. tabs) to the user. The initial implementation might look something like this, within the header.html.erb view:

<!-- header.html.erb -->
<div class="tabs header-tabs">
<%= link_to 'Home', root_path, class: "tabs--link #{'is-active' if is_tab_active?(:index)}" %>
<% if can? :view, Book %>
<%= link_to 'Books', books_path, class: "tabs--link #{'is-active' if is_tab_active?(:book)}" %>
<% end %>
<%= link_to 'About', about_path, class: "tabs--link #{'is-active' if is_tab_active?(:about)}" %>
<%= link_to 'Contact', contact_path, class: "tabs--link #{'is-active' if is_tab_active?(:contact)}" %>
</div>

We're using the popular and powerful CanCan gem to make it easy to check permissive behaviors, but doing so also adds some additional logic within our view. We also reference the same ApplicationHelper#is_tab_active? method numerous times:

# application_helper.rb
module ApplicationHelper
def is_tab_active?(tab)
case tab
when :about
return true if controller_name == 'about'
when :book
return true if controller_name == 'books'
when :index
return true if controller_name == 'index'
when :contact
return true if controller_name == 'contact'
else
false
end
end
end

Arguably, this is maintainable code, even with the no-no of including CanCan logic in the header view. However, this code is also far from beautiful and will eventually lead to problems down the road. What if we want to add additional tabs that are found in a different view? Maybe administrators on the site need to view some different tabs, which are created within the admin.html.erb view:

<!-- admin.html.erb -->
<div class="tabs admin-tabs">
<% if can? :edit, Profile %>
<%= link_to 'Edit Profile', edit_profile_path, class: "tabs--link #{'is-active' if is_tab_active?(:profile)}" %>
<% end %>
<% if can? :edit, Setting %>
<%= link_to 'Settings', edit_setting_path, class: "tabs--link #{'is-active' if is_menu_tab_active?(:settings)}" %>
<% end %>
</div>

Now, not only do we have to modify two different .erb view files when changing tabs, but the HTML structure of how tabs are created (i.e. <div class="tabs tab-XYZ">) is spread over multiple files as well.

The solution is to create our own view object, which is just a plain old Ruby object that acts as an interface, which can be extended through other classes to handle the actual view-based logic we need.

# view_objects/view_object.rb
class ViewObject
attr_reader :context

include Rails.application.routes.url_helpers
include ActionView::Helpers
include ActionView::Context

# CanCan integration.
include CanCan::ControllerAdditions
delegate :current_ability, :to => :context

def initialize(context, args = {})
@context = context
after_init(args)
end

def after_init(args = {})
end
end

Now, let's create a generic Tabs view object, which can be inherited by specific implementations (i.e. application components).

# view_objects/tabs.rb
class Tabs < ViewObject
def html
content_tag :div, tabs.join('').html_safe, class: 'tabs'
end

private
# Interfaced.
def tabs
fail 'must be implemented by subclass'
end

# Interfaced.
def active?(tab)
fail 'must be implemented by subclass'
end

# Gets formatted tab link CSS.
def tab_class(tab)
active_class = active?(tab) ? 'is-active' : nil
['tabs--link', active_class].compact.join(' ')
end

# Get full tab link_to.
def tab(text, path, tab)
link_to text, path, class: tab_class(tab)
end
end

# view_objects/tabs/header_tabs.rb
class HeaderTabs < Tabs
private

def tabs
[about_tab, book_tab, contact_tab, index_tab].compact
end

def active?(tab)
case tab
when :about
return true if controller_name == 'about'
when :book
return true if controller_name == 'books'
when :contact
return true if controller_name == 'contact'
when :index
return true if controller_name == 'index'
else
false
end
end

def about_tab
tab('About', about_path, :about)
end

def book_tab
# Perform privilege check inside method.
return nil unless can?(:edit, Book)
tab('Books', books_path, :book)
end

def contact_tab
tab('Contact', contact_path, :contact)
end

def index_tab
tab('Home', root_path, :index)
end
end

While the number of lines of code has slightly increased from the original implementation, we've dramatically reduced the complexity of how navigation tabs are handled in the codebase. We can now simply invoke the appropriate view object class in the actual view. For example, the header.html.erb code goes from this:

<!-- header.html.erb -->
<div class="tabs home-tabs">
<%= link_to 'Home', root_path, class: "tabs--link #{'is-active' if is_menu_tab_active?(:index)}" %>
<% if can? :view, Book %>
<%= link_to 'Books', books_path, class: "tabs--link #{'is-active' if is_menu_tab_active?(:book)}" %>
<% end %>
<%= link_to 'About', about_path, class: "tabs--link #{'is-active' if is_menu_tab_active?(:about)}" %>
<%= link_to 'Contact', contact_path, class: "tabs--link #{'is-active' if is_menu_tab_active?(:contact)}" %>
</div>

... to this:

<!-- header.html.erb -->
<%= HeaderTabs.new.html %>

Plus, encapsulating all the logic into separate view objects makes it much easier to modify existing tabs in the future, or even add new sections. For example, to add the admin tabs section we just need to implement the base Tabs interface:

# view_objects/tabs/admin_tabs.rb
class AdminTabs < Tabs
private

def tabs
[profile_tab, setting_tab].compact
end

def active?(tab)
case tab
when :profile
return true if controller_name == 'profile'
when :setting
return true if controller_name == 'setting'
else
false
end
end

def profile_tab
# Check if user can edit Profile.
return nil unless can?(:edit, Profile)
tab('Edit Profile', edit_profile_path(context.current_user), :profile)
end

def book_tab
# Check if user can edit Settings.
return nil unless can?(:edit, Setting)
tab('Settings', edit_setting_path, :setting)
end
end

<!-- admin.html.erb -->
<%= AdminTabs.new.html %>

Extract Policy Objects

As you may recall, in our previous article with refactoring fat models in Rails we discussed creating service objects. A service object is essentially a class that encompasses complex behaviors, typically across multiple models. More importantly, service objects inherently deal directly with data (i.e. ActiveRecord objects), so invoking a service object will usually force actual changes to the database. However, there are some scenarios where you may not need to change data, but must still invoke multiple models or complex behavior to retrieve some particular information. For such scenarios, a policy object is ideal.

A policy object can be extracted from your application code by identifying anywhere where one or more business rules are used to determine something about data that is already in memory. For example, consider the following snippet from our previous article, in which we created a simple service object called BookCleanup that updates the featured flag of all Books that have a low rating score:

class BookCleanup
def initialize
end

# Remove featured flag from all books with rating below 3.
def cleanup
Book.where('rating < ?', 3).update_all(featured: false)
end
end

Clearly, we can see that update_all(featured: false) causes this #cleanup method to modify the database. However, what if we just wanted to determine if a particular Book is considered "highly rated" (i.e. it has an average rating score of at least 4.5)? We might implement a BookPolicy object:

class BookPolicy
def initialize(book)
@book = book
end

def highly_rated?
@book.featured? && @book.rating >= 4.5
end
end

Now, wherever we need to check if a Book is highly rated, we can create a BookPolicy instance and invoke the #highly_rated? method. Moreover, we can expand the functionality of the BookPolicy object when we want to include additional checks, such as if a Book was published in the last year:

class BookPolicy
def initialize(book)
@book = book
end

def highly_rated?
@book.featured? && @book.rating >= 4.5
end

def published_within_last_year?
@book.published_on >= (DateTime.now - 1.year)
end
end

Decorate with Decorators

The last refactoring technique we'll cover today is making decorators out of existing callbacks. A decorator object essentially allows you to modify or "add onto" the behavior of existing objects, such as ActiveRecord models, without adjusting the behavior of outside code. Extracting decorators from existing code is typically useful when your model has been given too much responsibility, or where some behavior must only execute under certain circumstances.

For example, consider what happens when a user creates a new Book in our book-based application. We might want to trigger a tweet on Twitter on the user's behalf, letting their friends know they just read the newly-added Book. Such logic doesn't belong in the Book model, since Twitter handling falls outside its purview. However, a decorator object is perfect for this scenario:

class TwitterBookNotifier
def initialize(book)
@book = book
end

def save
@book.save && tweet
end

private

def tweet
Twitter.tweet(text: "I just finished reading '#{@book.title}' by #{@book.author}!")
end
end

Here we've created the TwitterBookNotifier class. As you can see, it's quite simple. It expects a Book argument passed during initialization, and it provides a #save method, which is similar to the normal Book#save method we'd be using. However, in addition to calling #save on the passed Book instance, it also invokes the #tweet method, which connects to the Twitter API and sends out a tweet.

Now, to make use of the TwitterBookNotifier object we can use it in our controller, just as we'd use an actual Book instance that is being created and saved:

class BooksController < ApplicationController
# ...

def create
@book = TwitterBookNotifier.new(Book.new(book_params))

if @book.save
redirect_to root_path, notice: "Your read book has been saved."
else
render "new"
end
end

# ...
end

There we have it! A handful of cool, new tips for refactoring rails models! Also, be sure to check out Airbrake's Rails exception handling gem, which simplifies the error-reporting process for all of your Ruby web framework projects, including Rails, Sinatra, Rack, and more. Built on top of Airbrake's powerful and robust Airbrake-Ruby gem, Airbrake provides your team with real-time error monitoring and reporting across your entire application. Receive instant feedback on the health of your application, without the need for user-generated reports or filling out issue tracker forms. With built-in integration for Ruby web frameworks, Heroku support, and even job processing libraries like ActiveJob, Resque, and Sidekiq, Airbrake can be integrated into your application and begin revolutionizing your debugging workflow in just a few minutes. Start using Airbrake today with a free 14-day trial.

Written By: Frances Banks