Removing the @
Hack in Rails Controllers
A while back, I wrote an article that described a collection of non-conventional strategies that I use to make Rails controllers more maintainable. While I still think all those thoughts are good, there is one in particular that I like so much I think it should become the standard way to do things in Rails.
Towards that goal, I am writing this article to make a case for why we should change how data is loaded in controllers, shared across actions and communicated to the view.
The Rails Way
Before I present my case, I think it is important to understand the “Rails Way”, what is clever about it and where it is deficient. Hopefully, this will make it clear why my proposal is better.
A verbose (non-Rails) way of loading data and communicating that data to the view is:
def show
user = User.find params[:id]
render :show, locals: { user: user }
end
Luckily, the Rails developers realized that every action would end with something similar to the last line and it would only be more annoying to write the more data we needed to pass to the template. They came up with two clever solutions to reduce this boilerplate:
- The
@
variable hack - Implicit rendering
Lets see what our action looks like with these two conventions in place:
def show
@user = User.find params[:id]
end
Implicit rendering renders the template named after the action. I love this convention and want to keep it. The @
variable hack is what I have a problem with.
The Hack
What is the @
variable hack? Instead of passing the variable to the view via some sort of hash they decided it would be better if we could just mark the variable.
How do we do that marking? Ruby itself does have two ways to mark variables. A variable can be prefixed with the $
or @
symbol.
The $
symbol is no good as Ruby would then make that value global. What about @
? Normally this indicates the variable should be shared among all method invocations operating on the same instance. This is supported natively by Ruby and most object-oriented languages call them instance variables.
The Rails developers realized that instance variables don’t have much purpose in controllers. Conceptually (from the developer perspective) a controller is basically:
controller = UsersController.new request
response = controller.show
I realize internally it is more than that, but conceptually it is:
- create an instance
- call a single method
- throws away the object.
This is more functional in nature and less object-oriented.
Since our controller object doesn’t stay around for long, and we only call one method, instance variables don’t have much purpose in controllers. The Rails developers saw this and decided to co-opt the @
marker for another purpose.
Although they are still technically instance variables, Rails sees these variables and copies them to the view. This removes the need to specify a hash of variables to pass to the view.
I call this a hack because we are using a feature for an unintended purpose. That doesn’t mean I disapprove because it is a hack. It is a clever use of an unused feature to reduce boilerplate. My problem is not the hack itself, but in practice it just doesn’t satisfy all our needs.
The Problems
What are the problems with the @
variable hack? The problem is not in co-opting the @
marker, but in sharing loading patterns across actions. We often have actions that have similar data needs. For example:
def show
@user = User.find params[:id]
enddef edit
@user = User.find params[:id]
end
Now do that for update
, destroy
, and any other member actions also. It gets very repetitive. Rails has a solution via callbacks. Instead of the above we might have:
before_action(only: %i[show edit]) { @user = User.find params[:id] }def show
enddef edit
end
There are problems with this solution:
- Targeting only certain actions with the
only
andexpect
options is error prone. I have often seen applications that are loading the wrong data because they didn’t maintain those lists well. - It it difficult to see that the action is actually doing something. Just looking at the action itself it appears to be doing nothing.
- It might be hard to catch the callback in a large real world controller. Especially if the callback is defined in a parent class or included module!
- Source order becomes important as some
before_action
hooks might need data from otherbefore_action
hooks.
Just Use Methods
My solution is conceptually pretty simple and can be summed up as “just use methods”.
As we consider all our needs, my proposal will quickly build up a good bit of boilerplate. Boilerplate is what the @
hack is trying to remove so this might seem like a regression. Stick with me for a minute and see if you like the ideas. At the end, we are going to wrap up all that boilerplate in a small bit of meta-programming to make it concise while still keeping the advantages.
Define a Method
In a typical object-oriented system, if we want one component to be able to get data from another component what is the simplest solution? The first component sends a message to the second component who responds with the information. This is one of the core idea of object-oriented programming. Message passing via methods.
What if instead of trying to send data to the view, we invert it and have the view ask the controller for the data it needs. So the view is sending a message asking for the data which the controller then provides it in a response.
This might look like the following in the view:
<%= controller.user.name %>
In the controller we might have:
def user
User.find params[:id]
end
This is just a public method; regular object oriented ideas. This method can be used by different templates. If the edit
and show
template both need the user, they can both call the same method. If the index
method doesn’t need it, then it doesn’t call it. We don’t load data that we don’t need. No need to maintain a list of only
and except
in a callback definition.
Memoize
We might want to print out a bunch of user attributes and we don’t want the controller loading a new instance every time, so lets memoize the loading:
def user
@user ||= User.find params[:id]
end
Note, I am using an instance variable, but I am using them for their real purpose (sharing the data between different method invocations on the same instance). This doesn’t cover the nil
case. A more complete solution is:
def user
return @user if defined? @user
@user = User.find params[:id]
end
A Helper
Now this is conceptually no longer a hack, but it is more verbose in our template (also our controller but we will get to that later). We don’t want to prefix every variable access with controller.
. What if we created a helper method to proxy to the controller:
module UsersHelper
def user
controller.user
end
end
Now our template is just:
<%= user.name %>
This is nicer, but creating a helper method for every one of these data loading methods could get annoying. Luckily, Rails has a solution for us. We can call helper_method
with any controller method and Rails will create the helper method for us. Now the data loading in our controller looks like:
def user
return @user if defined? @user
@user = User.find params[:id]
end
helper_method :user
Assignable
To better share loading behavior between actions, I have also found it helps for these methods to be assignable. Let us imagine we need some access control. We might defined the following:
def users
return @users if defined? @users
@users = policy_scope User.all
end
helper_method :usersdef user
return @user if defined? @user
@user = users.find params[:id]
end
helper_method :user
I am using one of these data loading methods (users
) to assist in the implementation of another method (user
). Unlike the before_action
callback, the source order doesn’t matter. Since we are just calling methods, we could define user
first.
This all seems well and good, but what if we also want our index
action to allow searching? We could define our index action like this:
def index
@users = users.search params[:q]
end
Now we are back to using the @
hack. If we put the search in the users
data loading method, then our show
action will also end up searching due to our implementation of user
. The general problem is that we want to share some of the data loading behavior between actions (the policy_scope
) but not other aspects (the searching).
The solution is assignment. What if we define another method that allows an action to refine the data:
privatedef users= assigned_users
@users = assigned_users
end
Now our index
action can be defined as:
def index
self.users = users.search params[:q]
end
Due to our memoization using the same instance variable (here again we are using instance variables for their purpose to share data between method invocations on the same instance) when the index
view calls users
it will get a list with the policy scope and search applied. When the show
action calls user
it will just get the policy scope filtering and not the search.
I made this assignment method private because I only use it within the actions (or a before_action
hook). I have never found a reason to use it in outside components (such as the view).
Testing
Another huge advantage of this proposal is testing. These methods are now public methods so we can call them in our test. Consider a test to see if the index
action is really searching. Before we might have had something like this (using rspec syntax and factorybot):
it 'searches for the given query' do
create :user, last_name: 'Doe'
create :user, last_name: 'Jackson' get '/users', params: { q: 'Doe' } expect( response.body ).to have_content 'Doe'
expect( response.body ).not_to have_content 'Jackson'
end
Here we are trying to imply the controller did the right thing by looking for keywords in the rendered template. What if some part of the page accidentally had the term Jackson
for an unrelated reason. Our test would fail not because the functionality is not working, but just a false positive.
Let us look at the same test using our public method:
it 'searches for the given query' do
expected = create :user, last_name: 'Doe'
create :user, last_name: 'Jackson' get '/users', params: { q: 'Doe' } expect( controller.users ).to eq [expected]
end
Here our test is stronger. We are ensuring the desired record was found and that was the only record found. No side effect can accidentally cause a false positive.
Reducing the Boilerplate
You might have been following along and thinking that you like what I am suggesting (hopefully), but there is the nagging boilerplate issue. Our data loading methods are now in total:
def user
return @user if defined? @user
@user = User.find params[:id]
end
helper_method :userprivatedef user= assign_user
@user = assign_user
end
Ugh! Luckily, Ruby is really good at simplifying common patterns via meta-programming. The two things we really need to provide are:
- The name of the data variable
- How to load that variable
In an effort to provide some symmetry with rspec (so it is familiar to other developers) I have named my method let
. Here is what the data loading looks like with the meta-programming in place:
let(:user) { User.find params[:id] }
Concise, but still with all the advantages of the boilerplate version. What does that meta-programming look like?
Drop this in your app/controllers/concern
directory and extend
it into ApplicationController
and you’re set. It is just a tiny bit of code. No need for a big library. Don’t like the name let
, then change it.
An Example
What does a controller look like with this construct in place?
I like to remove the trivial differences between creating and updating so I name the template form
and don’t have dummy templates that just render a partial. But you don’t need to do that if you don’t want.
The let
construct doesn’t mean you have to change anything else about your controllers. Code them the way you want. let
just shares loading of data better across actions, between the controller and view and makes your testing easier.
Influence
I do want to acknowledge the influence of the decent_exposure library on my proposal. This library first exposed me (no pun intended) to the idea. What I don’t like about decent_exposure is the implicit loading. It hides too much and it to difficult to customize.
I considered using that library, but never using the implicit loading. When I realized I just needed a small bit of meta-programming vs a big library I decided the concern was a better route.