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>
@post available in the
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
@postwhereas in a partial, you have to either depend on
@postbeing available, or you reference a local called
post, which is inconsistent with
- 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.
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 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
Lastly, I’ve opted to use
find_by_id instead of
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.