Rails Controller Accessors
by Nick Gauthier on
Recently, I’ve been reading Practical Object-Oriented Design in Ruby by Sandi Metz (I highly recommend it!) and it got me thinking more about OO design in Rails. I realized that one of the patterns I’ve been using synced really well with the messages in the book, and I wanted to share it.
Model Loading Filters
One of the most common Rails controller patterns is the before filter that fetches the record being accessed during member route actions. It looks like this:
class PostController < Application Controller before_filter :find_post, only: [:show, :edit, :update, :destroy] # actions redacted private def find_post @post = Post.find params[:id] rescue ActiveRecord::RecordNotFound flash[:error] = "No Such Post" redirect_to posts_path return false end end
<h1><%= @post.title %></h1>
This makes @post
available in the show
, edit
, update
, and destroy
actions, as well as making them available in the views.
However, there are a few problems with this implementation:
- In views, you access the post as
@post
whereas in a partial, you have to either depend on@post
being available, or you reference a local calledpost
, which is inconsistent with@post
. - The before filter has two responsibilities: finding the post and handling when there is no such post. Ideally this would be extracted into two methods each with one responsibility.
- Instance variable copying is The Rails Way, but it’s very different from The Ruby Way and Object Oriented best practices. In short, the view depends on the internals of the controller.
Controller Accessors
Recently, I’ve been using Controller Accessors as an alternative. Here’s what a Controller Accessor looks like:
class PostController < Application Controller before_filter :ensure_post, only: [:show, :edit, :update, :destroy] # actions redacted private def post @post ||= Post.find_by_id params[:id] end helper_method :post def ensure_post redirect_to posts_path, alert: "No Such Post" unless post end end
<h1><%= post.title %></h1>
Here we’ve extracting the post
finding to a separate accessor method with memoization. We also expose this accessor as a helper method, which is a great way to allow the view to access a controller’s properties.
Note that in the view we access post
and not @post
. @post
is still available, but accessing it via post
is the polite way to do it, and it much more resilient.
We’ve also separated loading the post and ensuring that it is available by moving our filter to ensure_post
.
Lastly, I’ve opted to use find_by_id
instead of find
because find_by_id
returns nil instead of raising ActiveRecord::RecordNotFound
, and a nil
is easier to test for. Also, it wouldn’t make sense for the post
accessor to throw an exception (or to rescue it!) we’d rather have a nil
to express that there is no such post.