Posted on

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 %>

This is a great way to decouple the logic of the homepage from our controller completely. Now, it’s much easier to test both the controller and the homepage logic in isolation. When new homepage requirements come along, the changes will happen inside the Home class and the view, the controller most likely won’t even change at all.


If you like this post, follow me:


Or share this post: