Article

DRYing up Rails Controllers: Polymorphic & Super Controllers

Josh Symonds

This post was previously on the Pathfinder Software site. Pathfinder Software changed its name to Orthogonal in 2016. Read more.

Controllers are an obvious point to find repeated behavior in an application. Identical methods across many controllers are a very common problem: consider comments, for example, where you might encounter an add_comment and remove_comment in every controller. Or think of all the controllers that render the same actions in the same way. Maybe they implement very basic CRUD functionality but have very complicated AJAXy tricks. You find yourself typing in those tricks over and over again on every controller. Let’s fix that! By using a few neat techniques, we can significantly DRY up our controllers and end up with code that’s not only very reusable, but very easily changeable to effect significant alterations in our application.Let’s start with the first problem: identical actions across many controllers. What we really want is a controller that responds to many different requests the same way, without caring what those requests are as long as they fit our rather generalized expectations. This is an instance of polymorphism. But is there a good way to implement it in controllers? While there is, before we start, let’s take a quick look at how Rails implements polymorphism in models to get an idea

Let’s start with the first problem: identical actions across many controllers. What we really want is a controller that responds to many different requests the same way, without caring what those requests are as long as they fit our rather generalized expectations. This is an instance of polymorphism. But is there a good way to implement it in controllers? While there is, before we start, let’s take a quick look at how Rails implements polymorphism in models to get an idea of how we can implement it ourselves.

Polymorphic Models

Many Rails models have a polymorphic relationship with other models. Think of comments, for example. If you wanted to implement comments on your website, then you’d want to apply them everywhere; for example, it would be nice if you could add comments to users, and projects, and events, and meetings… well, to more or less everything. In this use case, we call comments polymorphic: they can belong to many different models. This is an easy relationship to establish in Rails:

app/models/comment.rb
class Model < ActiveRecord::Base
  belongs_to :commenter, :polymorphic => true
end

app/models/user.rb
class User < ActiveRecord::Base
  has_many :comments, :as => :commenter
end

app/models/project.rb
class Project < ActiveRecord::Base
  has_many :comments, :as => :commenter
end

Each model has_many :comments as :commenter. Comment.rb in turn says it belongs to a :commenter with a polymorphic relationship. As long as the comments table has a commenter_type string field and commenter_id integer field, Rails fills in all the details, and you can code:

user.comments.create(:body => "This is a user comment.")
post.comments.create(:body => "But this is a post comment.")

With equal ease. While this solution is extremely elegant it starts to fall apart in the controllers (which is why this post is about controllers, not about models). There’s no natural polymorphic solution for controllers, so I frequently see repeated code across controllers with methods like add_comment, edit_comment, and remove_comment on the users controller, the posts controller, and all other controllers where comments may be added to their model.

While it may seem like the obvious solution is to create a module and mix it in to the controllers with the comment code, there’s an even easier way to DRY this up: make your controller as polymorphic as the model.

Polymorphic Controllers

A polymorphic controller works exactly like a polymorphic model. It receives a type and an ID just like the model does: using the type it establishes the model class, and using the ID it finds a member of that class (specifically, the one which we are adding the comment to). Let’s continue with our previous example and implement a fully-functioning comments controller for a polymorphic model called Comment.

We know that this controller will be receiving two params all the time: commenter_type and commenter_id. We can use these to find the model object that we want to add comments to before we do anything else.

app/controllers/comments_controller.rb
class CommentsController < ApplicationController
  before_filter :find_commenter

  private

  def find_commenter
    @klass = params[:commenter_type].capitalize.constantize
    @commenter = klass.find(params[:commenter_id])
  end
end

Using the handy-dandy Rails helper constantize, we convert the string representation of the class (once it is correctly capitalized) into the constant referring to the class itself. Afterwards it is simplicity itself to find the actual commenter by finding on its class. Let’s capitalize on our success and add in a create action.

app/controllers/comments_controller.rb
class CommentsController < ApplicationController
...
  def create
    @comment = @commenter.comments.create(params[:comments])
    respond_to do |format|
      format.html {redirect_to :controller => @commenter.class.to_s.pluralize.downcase, :action => :show, :id => @commenter.id}
    end
  end
...
end

This example assumes that the other controller classes are named for their pluralized models (which is also Rails’ assumption). Now it is easy for us to add in code to every view that we want to create a comment in:

app/views/users/show.html.erb
<-- User show code here -->
<% form_for @comment, :url => comments_path(:commenter_type => "user", :commenter_id => @user.id) do |f| %>
  <%= f.text_field :body %>
<% end %>

app/controllers/users_controller.rb
def show
  @user = User.find(params[:id])
  @comment = Comment.new
end

That comments_path is rather ugly, though. Allow me to suggest a small helper to improve the readability of your code at the cost of some convention expectations on your part:

app/helpers/application_helper.rb
def commenter_url
  commenter = controller.controller_name.singularize
  comments_path(:commenter_type => commenter, :commenter_id => controller.instance_variable_get("@#{commenter}").id)
end

The convention expectation here is that you will name an instance variable in your controller after that controller. For example, in the users_controller.rb I showed earlier, @user fulfills that requirement: it is an instance variable named after its own controller. Now the show view would look like this:

app/views/users/show.html.erb
<% form_for @comment, :url => commenter_url do |f| %>
  <%= f.text_field :body %>
<% end %>

Commenter_url automatically fills in the commenter_type as “user” (the singularized name of itself) and the commenter_id as @user.id (by getting the instance variable @user set on the controller and referencing its ID) and creates a link to a new comment. This helper is easily reusable across all of your controllers and saves you the trouble of retyping the same comments_path every time you want to make a form for a new comment.

Super Controllers

Sometimes you have rather basic CRUD-like functionality in many controllers. All of that functionality is supposed to render the same, regardless of the controller. For example, let’s say we have a meeting model and a user model. Meetings and users are different, but in our system they’re going to end up being shown in exactly the same way: we render the action that they respond to, or if we received an xhr request we update a specific dom element on the page.

This is a perfect use case for super controllers. All Rails controllers already descend from a super controller: the application controller, held in app/controllers/application.rb. It is trivial for us to extend this descent hierarchy another step, and by doing so we can sometimes save ourselves a significant amount of time. Consider these two controllers:

app/controllers/users_controller.rb
class UsersController < ApplicationController
def update
  @user = User.find(params[:id])
  @user.update_attributes(params[:user])
  respond_to do |format|
    format.html {redirect_to :action => :show, :id => @user.id}
    format.js {render :update do |page|
      page.replace_html dom_id(@user), :partial => "user"
    end}
  end
end

app/controllers/meetings_controller.rb
class MeetingsController < ApplicationController
def update
  @meeting = Meeting.find(params[:id])
  @meeting.update_attributes(params[:meeting])
  @meeting.send_edit_notification # Inform the meetings' participants that it has been editted
  respond_to do |format|
    format.html {redirect_to :action => :show, :id => @user.id}
    format.js {render :update do |page|
      page.replace_html dom_id(@user), :partial => "user"
    end}
  end
end

It should be fairly obvious exactly where these methods are the same. But we can’t use the same controller for both of them: if we did then the @meeting.send_edit_notification wouldn’t be called correctly. The solution is a super-controller that encompasses both of them:

app/controllers/users_controller.rb
class UsersController < GenericController
def update
  @user = User.find(params[:id])
  @user.update_attributes(params[:user])
  super
end

app/controllers/meetings_controller.rb
class MeetingsController < GenericController
def update
  @meeting = Meeting.find(params[:id])
  @meeting.update_attributes(params[:meeting])
  @meeting.send_edit_notification # Inform the meetings' participants that it has been editted
  super
end

app/controllers/generic_controller.rb
class GenericController < ApplicationController

protected

def update
  instance_variable = instance_variable_get("@#{controller_name.singularize}"
  respond_to do |format|
    format.html {redirect_to :action => :show, :id => instance_variable.id}
    format.js {render :update do |page|
      page.replace_html dom_id(instance_variable), :partial => controller_name.singularize
    end}
  end
end

Both UsersController and MeetingsController now descend from the Generic Controller, whose methods are protected (so that only children or itself may access them). Both of the children controllers call super at the end of their method, which evaluates the method of their parent in their context. The parent finds the instance variable we set earlier and then responds correctly based on it. (Interestingly we could even have moved the update_attributes into the parent update method with only a bit of finagling, but I think my point has come across).

Note that the amount of work we’ve done to extract update to a parent class is minimal, while the take away is huge. If the respond_to block was very large or complicated, as it may sometimes be, being able to have it all in one place to edit for both controllers at once is a huge boon and a great time-saver.

While I’ve made great use of polymorphic controllers in the past, I’ve only just started exploring the possibilities that super controllers present in a recent project. They saved me a great deal of time and have allowed me to considerably extract some code that otherwise would have been repeated across three different controllers — and that’s only a start. By employing polymorphic controllers and super controllers, your Rails code will improve immeasurably, in both readability and maintainability. I hope you find these techniques as useful and elegant as I have.

Related Posts

Article

Help Us Build an Authoritative List of SaMD Cleared by the FDA

Article

SaMD Cleared by the FDA: The Ultimate Running List

Article

Roundup: Bluetooth Medical Devices Cleared by FDA in 2023

White Paper

Software as a Medical Device (SaMD): What It Is & Why It Matters