Even More Tips for Refactoring Rails Models

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:

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:

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:

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.

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

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:

… to this:

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:

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:

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:

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:

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:

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:

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. Have a look below at all the features packed into Airbrake and start improving your exception handling practices today!