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:
- Client-server architecture
- Statelessness
- Cacheability
- Layered system
- Code on demand
- Uniform interface
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 |
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 Group
s.
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 User
s 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:
- Pay attention to the model being actioned
- Extend vocabulary via Classes, not actions
- Uncover new domain models
References
- Rails docs on resourceful routing
- Wikipedia entry on REST
- Toptal article on service objects
- AppSignal blog post on controller patterns and anti-patterns
- 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 …