Controller filters are a common sight in Ruby on Rails controllers. The
before_filter specifically can be used to halt the request cycle. This is useful to prevent unauthorized access to controller actions, but it’s also very often used to load database records.
Take a look at this controller:
class TreesController before_action :authenticate_user! before_action :find_forest before_action :find_trees before_action :find_tree, except: [:index, :new, :create] def index end def show end def edit end def update if @tree.update(tree_params) redirect_to(@tree) else render('edit') end end private def find_forest @forest = Forest.find(params[:forest_id]) end def find_trees @trees = @forest.trees end def find_tree @tree = @trees.find(params[:id]) end end
before_action filters have been used to make the controller DRY. The forest and trees are loaded by the filters and assigned to instance variables.
The views for
edit have access to the
@tree instance variable, but this isn’t obvious. You have to look at the controller and connect the dots to figure out which variables are available where.
Perhaps the controller has become too DRY. The filters are coupled to the controller actions in a very weird way. In fact, the
:find_tree filters are coupled to each other as well, because
:find_tree depends on
:find_trees. In terms of the problem domain, it becomes hard to see the forest through the trees (pun intended).
In other words, clarity has been sacrificed for the sake of DRY-ness. Don’t get me wrong, DRY is a great concept, but not when it turns your Rails controller into a complicated puzzle!
In my opinion, before filters should only ever be used to do what they do best: halting the request cycle. They should not be used for data loading and preparing state.
Applying that to our controller would result in something like this:
class TreesController before_action :authenticate_user! def index @trees = find_forest.trees end def show @tree = find_tree end def edit @tree = find_tree end def update @tree = find_tree if @tree.update(tree_params) redirect_to(@tree) else render('edit') end end private def find_tree find_forest.trees.find(params[:id]) end def find_forest Forest.find(params[:forest_id]) end end
The filters which were previously loading data have been replaced with plain method calls from the controller actions. Now it’s immediately obvious which data is loaded where.