Rails supports a RESTful interface through its resourceful routing. Sticking to these resourceful defaults can help to avoid bloated controllers and can also help to uncover hidden domain models.

Introduction

For Rails applications, controller actions offer a gateway to your business logic. With convenient access to both the request and response objects, they tempt the unwary developer into bad patterns which see too much logic stuffed into these action methods, or their associated filters. This is often known as the fat-controller antipattern.

If we want to avoid the slippery slope into over-weight controllers we need to be vigilant. We can address the bloat using a number of well-documented techniques:

  • Push more logic into our ActiveRecord models
  • Craft Query objects to encapsulate the logic for fetching records
  • Introduce Service objects to encapsulate a business action
  • Leverage Form objects to handle input params and validations.

But none of these patterns focus on the role of resourcefulness in keeping controllers maintainable.

A brief discussion on RESTful and resourceful

REST stands for Representational State Transfer. It is an architectural style for requesting and exposing content over a network. In order for an interface to be considered RESTful it must conform to a number of constraints:

  1. Client-server architecture
  2. Statelessness
  3. Cacheability
  4. Layered system
  5. Code on demand
  6. Uniform interface
We will not go into details on these constraints here, the interested reader can find out more on wikipedia. But many of these constraints just express how we generally understand the web to work. For instance HTTP is the protocol which is typically used to request and transfer resources over the web, at its heart it is a client-server protocol. It is also is a stateless protocol, as each request is independent of the last; this is the reason we are forced to maintain sessions on the server. With sessions, we essentially adopt some convention, whereby some particular data in the incoming HTTP request is used to tie multiple requests together, and we group them together with the concept of a session. It is the statelessness of HTTP that allows us to cache responses to GET requests on CDNs, or at any other layer that the request passes through. And talking of layers, when our end-user requests a resource over the web, they generally don't care through how many servers their request has passed before they receive it. This intrinsic layering in the web, via proxies and load-balancers, underpins the architecture of the web and is essential for its scalability.

Rails promotes the use of the standard HTTP verbs (GET, POST, PUT, PATCH and DELETE) to define a uniform interface to manage the entities (or resources) exposed by your application.

Rails offers support for RESTful URLs via the concept of resources. Your application exposes an API to interact with different resources. The API you expose is defined by the use of the resource and resources methods in your routes file. Pulling the example straight from the Rails docs, consider the following resourceful declaration in your config/routes.rb:

  
Rails.application.routes.draw do
  …
  resources :photos
  …
end
   
Generally your routes file will declare a mapping: (HTTP verb + URL) => controller action. For a resourceful declaration, such as the one shown for :photos, Rails will automatically map to seven RESTful actions as follows:
HTTP Verb Path Controller#Action Used for
GET /photos photos#index display a list of all photos
GET /photos/new photos#new return an HTML form for creating a new photo
POST /photos photos#create create a new photo
GET /photos/:id photos#show display a specific photo
GET /photos/:id/edit photos#edit return an HTML form for editing a photo
PATCH/PUT /photos/:id photos#update update a specific photo
DELETE /photos/:id photos#destroy delete a specific photo
The basic principle is that all operations in your application should be achieveable using one of these seven routes for some resource defined by your application. Rails uses the term resourceful routing for routes that follow this pattern. If you are exposing URLs that do not fit this pattern then it is not resourceful and, possibly, not RESTful.

In this article I will look at using a resourceful approach to slim down a controller that is trying to do too much. I will examine a particular example with some non-resourceful routes and I will outline three guiding principles which should help us to trim down the responsibilities of such controllers. At the same time, we will be moving to a more resourceful interface for interacting with our application.

Our Fat-Controller

The controller we are going to examine will expose a number of actions on a fictitious Group model:

    
class GroupsController < ApplicationController
  def index
    @groups = Group.all
  end

  def show
    @group = group
  end

  def list_users
    @users = group.memberships.map(&:user)
  end

  def leave_group
    group.memberships.destroy(id: params[:membership_id])
  end

  def list_submission_scores
    group = Group.find(params[:id].to_i)
    @submission_scores = group.submissions.reduce({}) do |memo, submission|
      memo[submission.user_id] = [ memo[submission.user_id], submission.score ].max
    end
  end

  private

  def group
    @_group ||= Group.find(params[:id].to_i)
  end
end
    
A Group instance has_many :memberships, and our Membership model will link a particular User to a particular Group. In addition we consider that a Group instance has_many :submissions. Here the Submission model will represent some piece of work submitted by members of the group, on which they are scored. Together let's presume that our models look something like this:
    
# app/models/group.rb
class Group < ApplicationRecord
  has_many :memberships
  has_many :assigments
  …
end

# app/models/membership.rb
class Membership < ApplicationRecord
  belongs_to :group
  belongs_to :user
  …
end

# app/models/submission.rb
class Submission < ApplicationRecord
  belongs_to :group
  belongs_to :user
  validates :score, presence: true
  …
end
    
We do not intend to change the models in our discussion, but this should help to clarify the different relationships that exist between them. One thing that will change in our subsequent discussion will be the routes that are exposed by our application. At the outset this is how they look:
    
Rails.application.routes.draw do
  …
  resources :groups, only: [:index, :show] do
    get :list_users
    get :list_submission_scores
    post :leave_group
  end
  …
end
    
The index and show actions are perfectly resourceful, so we will not concern ourselves with those. But the endpoints for list_users, list_submission_scores and leave_group suggest that we have lost our way a bit. Let's see if we can improve on this.

1. Pay attention to the model being actioned

Our first principle should help us to address the non-resourcedful route for the leave_group action. A quick look at the code in this action reveals that we are actually deleting a group membership, the Group instance is not actually being impacted. This function could be delivered RESTfully by using the destroy action on a MembershipsController. This might look as follows:

  
# config/routes.rb
Rails.application.routes.draw do
  …
  resources :groups, only: [:index, :show] do
    resources :memberships, only: [:destroy]
    get :list_users
    get :list_submission_scores
  end
  …
end

# app/controllers/memberships_controller.rb
class MembershipsController < ApplicationController
  def destroy
    group.group_memberships.find(params[:id].to_i).destroy
    redirect_to group_path(group)
  end

  private

  def group
    @_group ||= Group.find(params[:group_id]
  end  
end
  
In this refactor I have also made use of a nested path for the MembershipsController. This will mean that requests to this endpoint will need to specify a :group_id in the URL along with the actual :id of the membership. Passing this additional :group_id may be considered redunant, but I think it provides a more explicit and logical routing interface to nest the routes in this manner. In addition, I have then used the :group_id to scope the retrieval of the Membership instance before deleting, which I think is a worthwhile sanity check.

It may seem like a simple matter to see that this controller action (leave_group) is actually operating on a model other than the Group. Nonetheless, I suspect you will come across many similar cases where a non-resourceful controller method has been added which actually belongs in another controller - especially when the required controller does not yet exist. There is a natural gravity to add new behaviour to existing controllers, as a quick win, but with time these small non-resourceful actions will clutter up your controller and bog you down.

The signature of this behaviour is that you start to develop God-controllers, in which every action seems to belong. Once you start with non- resourceful actions, adding others just seems to make sense. We need an action to add a user to a group? Well we have an action for #leave_group, let's just add one for #join_group. You get the idea.

2. Extend vocabulary via Classes, not actions

To understand this principle lets consider the list_users action. Again, this is clearly not returning Group instances, it's returning User instances. A resourceful controller should only return representations of that single resource, i.e. our GroupsController should only be returning some representation of Groups.

We already have our UsersController, do we need to extend the index action to optionally take a :group_id, and return a different list of Users in this case? This is one option, but I would say it is not the best. We don't want to remove complexity from one place in our application just to make the code elsewhere more complex!

Consider what we are trying to say with our existing controller action: GroupsController#list_users. So we want a list of User instances, but we want them scoped to a Group. So lets set up a nested UsersController to give us exactly that:

  
# config/routes.rb
Rails.application.routes.draw do
  …
  resources :groups, only: [:index, :show] do
    resources :memberships, only: [:destroy]
    resources :users, only: [:index], controller: 'groups/users'
    get :list_submission_scores
  end
  …
end

# app/controllers/groups/users_controller.rb
class Groups::UsersController < ApplicationController
  def index
    @users = group.memberships.map(&:user)
  end

  private

  def group
    @_group ||= Group.find(params[:group_id]
  end  
end
  
Again we create a new controller to house a single index action. To make this transition we have realised the list_users action is trying to say something that it cannot say with the usual RESTful actions on the GroupsController. But rather than trying to introduce this new vocabulary as an action name, we really want to extend our vocabulary using our controller classes. Think of a controller class name that would give us what we want, and just use the RESTful actions on this new concept.

3. Uncover new domain models

In the two previous cases the controller actions under consideration were impacting or retrieving instances of existing domain models. Sometimes your non-resourceful controller action exists because the business domain model has not yet been conceived.

Looking at list_submission_scores we see that it is retrieving all of the submissions for a single Group and then it is looping through them to construct a Hash with the max score for each user.id. We are building a scoreboard. Why don't we introduce a new Scoreboard model, and retrieve it. We can again scope this to a particular Group and use a nested route to reflect this hierarchy:

  
# config/routes.rb
Rails.application.routes.draw do
  …
  resources :groups, only: [:index, :show] do
    resources :memberships, only: [:destroy]
    resources :users, only: [:index], controller: 'groups/users'
    resource :scoreboard, only: [:show], controller: 'groups/scoreboards'
  end
  …
end

# app/models/scoreboard.rb
def Scoreboard

  def initialize(group)
    group.submissions.each do |submission|
      self[submission.user_id] = [ self[submission.user_id], submission.score ].max
    end
  end
end

# app/controllers/groups/scoreboards_controller.rb
class Groups::ScoreboardsController < ApplicationController
  def show
    @scoreboard = Scoreboard.new(group)
  end

  private

  def group
    @_group ||= Group.find(params[:group_id].to_i)
  end
end
    
The Scoreboard model is not an ActiveRecord model, it is just a plain Ruby object which encapsulates the logic for retrieving your scoreboard for a given group. It is easy to test and easy to extend. The new controller just builds an instance of the Scoreboard model for the group in question and this will be passed off to be rendered as in the original implementation. The guiding principle here was being able to identify when there is a domain object that we are trying to use, or access, but we just haven't given it a name yet. Once you identify the missing domain concept, breaking it out into a RESTful controller endpoint is pretty straightforward.

Summary

In this article we have looked a how adhering to the resourceful routes baked into Rails can help to keep your controllers focussed and under-control. In particular we have looked at the task of refactoring a simple GroupsController using three guiding principles:

  1. Pay attention to the model being actioned
  2. Extend vocabulary via Classes, not actions
  3. Uncover new domain models
I hope these principles prove useful when refactoring your own fat-controllers.

References

  1. Rails docs on resourceful routing
  2. Wikipedia entry on REST
  3. Toptal article on service objects
  4. AppSignal blog post on controller patterns and anti-patterns
  5. Oozou blog post on maintainability in large rails apps

Comments

There are no existing comments

Got your own view or feedback? Share it with us below …

×

Subscribe

Join our mailing list to hear when new content is published to the VectorLogic blog.
We promise not to spam you, and you can unsubscribe at any time.