We’ve talked about Null Objects before, and how they can remove unwanted conditionals from your code. I’d like to talk about extending those benefits into your Rails views.
Setting the scene
Recently I’ve been working on an application that displays a lot of graphs of
financial data. A DataSeries
can return a graph of its recent history, but
only if it has_data?
.
# app/models/users/data_series.rb
class DataSeries < ActiveRecord::Base
def recent_history_graph
if has_data?
Graph.new(self)
end
end
def has_data?
# ...
end
end
In the view, the graph is rendered like this:
# app/views/data_series/show.html.erb
<% if @data_series.recent_history_graph %>
<%= render @data_series.recent_history_graph %>
<% end %>
This code isn’t too bad, but it could be better. The has_data?
check is
already happening inside the recent_history_graph
method, so the client code
doesn’t need to know under what conditions a DataSeries
can produce a Graph
,
but it still needs to check if a graph was produced.
There are a lot of graphs in this application, and all these conditionals are
cluttering up my view code. They introduce additional coupling between the
view and the model; every time I call recent_history_graph
I have to be aware
of two possible return types.
Null Object to the rescue
By refactoring the code to use a Null Object I can remove all those pesky conditionals once and for all.
First, I’ll remove the conditional:
# app/views/data_series/show.html.erb
<%= render @data_series.recent_history_graph %>
Without checking for nil
, whatever is returned by recent_history_graph
is
passed to render
, so my tests start yelling at me about trying to render
nil
. To get around this problem I can change the recent_history_graph
method
to make sure that it never returns nil
:
# app/models/data_series.rb
class DataSeries < ActiveRecord::Base
def recent_history_graph
if has_data?
Graph.new(self)
else
NullGraph.new
end
end
# ...
end
So far so good. Now my tests are yelling about a non-existent NullGraph
class, so to shut them up I can add an empty class:
# app/models/null_graph.rb
class NullGraph
end
This is where things get really interesting. Instead of passing nil
to render
I’m passing an instance of NullGraph
. My tests reveal the following error:
NullGraph is not an ActiveModel-compatible object that returns a valid partial path.
render
can only handle objects that respond to the to_partial_path
method. The usual solution to this error is to include the
ActiveModel::Conversion
module into the class. It provides an implementation
of to_partial_path
based on the class name. In this case it would return
"null_graphs/null_graph"
. I don’t want the default behaviour though; I don’t
consider a NullGraph
to be a first-class object, worthy of its own
sub-directory to keeps its views in. I’d rather use the partial "graphs/null"
.
Fortunately, this can be achieved by defining my own to_partial_path
method:
# app/models/null_graph.rb
class NullGraph
def to_partial_path
'graphs/null'
end
end
We’re getting close! The tests are now complaining about a missing partial. The partial itself doesn’t need any code, but it’s nice to leave a comment there to guide future developers who stumble across a blank partial and wonder what on Earth is going on:
# app/views/graphs/_null.html.erb
<%# Blank partial for rendering NullGraph objects %>
Run the tests one more time, and we’re back to green with no more conditionals cluttering the view code. Ah, that’s better.
There’s more that could be done: The logic for deciding what kind of Graph
to
return probably doesn’t belong in DataSeries
, but I’ll save that refactoring
for another day.