A common practice in Ruby on Rails applications is to have a HomeController to render the homepage, which doesn’t directly translate into a resource.

Let’s say we’ve got a video sharing application, and its homepage needs to display some fancy information about the way the website works. Such a static page doesn’t really need a complicated controller at all:

class HomeController < ApplicationController def show respond_to(:html) end end

More can be done with the homepage than just displaying some info. We’d like to display daily picked videos on the homepage. Easy one, just add a line to the controller:

class HomeController < ApplicationController def show @todays_picks = Video.todays_picks.limit(6) respond_to(:html) end end

To stay up to date with ongoing trends, users also have to be able to view the most watched videos:

class HomeController < ApplicationController def show @todays_picks = Video.todays_picks.limit(6) @most_watched = Video.most_watched.limit(6) respond_to(:html) end end

Since everyone loves to watch crazy cat videos all day, we’ll feature them on the homepage too:

class HomeController < ApplicationController def show @todays_picks = Video.todays_picks.limit(6) @most_watched = Video.most_watched.limit(6) @cats = Video.with_funny_cats.limit(6) respond_to(:html) end end

It appears a lot of users visit the homepage very frequently, but the content on the homepage doesn’t change as much. Let’s add a performance improvement to make sure those returning visitors can load the page from their browser cache rather than fetching it again:

class HomeController < ApplicationController def show @todays_picks = Video.todays_picks.limit(6) @most_watched = Video.most_watched.limit(6) @cats = Video.with_funny_cats.limit(6) return unless stale?( last_modified: [ @todays_picks.maximum(:updated_at), @most_watched.maximum(:updated_at), @cats.maximum(:updated_at) ].max, etag: [ @todays_picks.ids, @most_watched.ids, @cats.ids ].join('-') ) respond_to(:html) end end

The controller quickly becomes bloated and complicated. Because all the logic is embedded in the controller, it’s very difficult to test. Since many instance variables are used, the interface between the controller and view is unclear and prone to changes. Future requirements might include something more complex than we’ve done so far, such as personal video recommendations. If the controller continues to grow the same way, the controller tests will surely become a living nightmare.

Sandi Metz once introduced rules for Ruby developers, one of which states that controllers instantiate a single object and views can only know about a single instance variable. In its current state, the controller clearly violates that rule.

The problem is that there’s not really a clear resource to use for the homepage, after all we’re displaying so many different kinds of videos on there. But to follow Ruby on Rails conventions, we can introduce a “pseudo resource”, which meets the minimal requirements to play nice with our controller the way Rails expects it to. The view can then use that single object instead of accessing many different instance variables.

We can consider the home to be a resource by itself. Since there’s only one homepage, it would be a singular resource. We can be implement the Home resource as a PORO (Plain Old Ruby Object):

class Home def cache_key "home/#{to_param}-#{updated_at.utc.to_s(:nsec)}" end def to_param @as_param ||= [ todays_video_picks.ids, most_watched_videos.ids, cat_videos.ids ].join('-') end def updated_at @max_updated_at ||= [ todays_video_picks.maximum(:updated_at), most_watched_videos.maximum(:updated_at), cat_videos.maximum(:updated_at) ].max end def todays_video_picks Video.todays_picks.limit(6) end def most_watched_videos Video.most_watched.limit(6) end def cat_videos Video.with_funny_cats.limit(6) end end

Since I don’t use these kind of objects frequently at all, I’d just place this class inside the models folder rather than label it as something like an “action object”.

Note that the instance variables which were previously in the controller have been moved to methods in the Home class. The view can now simply access those methods instead.

Both the cache_key and updated_at methods are used for the conditional GET we already had in the controller, but now the complexity of it is hidden from the controller. Now that we’ve moved the logic outside our controller, the controller itself becomes very simple:

class HomeController < ApplicationController def show @home = Home.new respond_to(:html) if stale?(@home) end end

Quite a difference! Now the controller is focused purely on its responsibilities.

By the way, the cache_key method is also useful for the view. Since the cache view helper will attempt to call cache_key on the object we pass in, it can be used to cache the homepage contents in a way consistent with the conditional GET in the controller:

<% cache(@home) do %> <%= render(@home.todays_video_picks) %> <%= render(@home.most_watched_videos) %> <%= render(@home.cat_videos) %> <% end %>