Rails controllers, views & variables
, Chicago
In object-oriented programming, an object uses instance variables to store private information. If you want to access an object’s instance variables, those variables should only be accessible through instance methods. At least, this is the expectation.
So when one dives into Rails, they may find it quite confusing when an instance variable set in a controller is also available in the view. Convenient, sure…but it somehow doesn’t feel true to Ruby. The controller and view are different objects with their own scopes, right?
How hard is it to be a purist? How can we tell Action Controller to be a little more discreet?
# config/initializers/less_gossip.rb
# Stop sharing so much!
ActionController::Base.class_eval do
def add_instance_variables_to_assigns() end
end
# Stop caring so much!
ActionView::Base.class_eval do
def assign_variables_from_controller() end
end
Now the controller has some privacy. Rails won’t force it to tell all its secrets to the view. The controller still needs to do its job, though, which likely involves retrieving data and serving it to the view. So how to we mediate the flow of information without instance variables? The same way Ruby usually does: attribute readers.
Let’s say we have a
CRUD
controller for articles. ArticlesController#index would
normally assign @articles, so let’s define a reader method,
articles, instead.
# app/controllers/articles_controller.rb
def articles
Article.all
end
Now we can call articles throughout the controller, and it will return what
@articles previously would have. Rails is pretty good about
caching database queries, but that doesn’t mean we shouldn’t memoize as a
best practice for optimization:
def articles
@articles ||= Article.all
end
Ah, instance variables used as they were meant to be used.
So what about @article? Doesn’t its value change depending on
the action being called? We just have to be a little smarter. Several lines
of @article = Article.find(params[:id]) wasn’t very
DRY, anyhow.
def article
@article ||= if params[:id]
Article.find params[:id]
else
Article.new params[:article]
end
end
There we go. We’re accessing variables through getters! When we’re in the
view, we only need to call those methods on the controller, which is
available through ActionView::Base#controller. Here’s an
example:
<%# app/views/articles/index.html.erb%>
<%= render controller.articles %>
What? That’s pretty ugly? And what about the controller public instance methods? They should refer to actions?
I guess we can’t completely ignore Rails’ conventions. We’ll have to make those getters private. But how, now, can we deliver these methods to the view? Luckily, Rails makes this easy with helpers:
# app/controllers/articles_controller.rb
helper_method :articles, :article
Now it looks nicer in the view, if a bit plainer than the @
decorations we’re all used to:
<%# app/views/articles/index.html.erb%>
<%= render articles %>
There are, actually, a few benefits to all this maneuvering (beyond clarity for the classically-trained Rubyist):
- Maintainability
- Any before filter you used, in the past, to set instance variables, is
now irrelevant. You no longer have to worry about scoping it with
:onlyor:except, or updating it when you add, remove, or rename actions. - Cacheability
- These methods will only run when they’re first called during a request. If they only appear in the view, and the view is cached, you won’t hit the database.1
Here’s our completed code: