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.
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
# 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
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.
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
render, so my tests start yelling at me about trying to render
nil. To get around this problem I can change the
to make sure that it never returns
# 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
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
method. The usual solution to this error is to include the
ActiveModel::Conversion module into the class. It provides an implementation
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
NullGraph to be a first-class object, worthy of its own
sub-directory to keeps its views in. I’d rather use the partial
Fortunately, this can be achieved by defining my own
# 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
return probably doesn’t belong in
DataSeries, but I’ll save that refactoring
for another day.