Example 3. Commonality and Inheritance

In our last example, we saw how Fortitude allows you to share code among methods in the same view and how it lets you pass blocks to allow very flexible reuse and customization of that code.

Here, we’ll explore how Fortitude allows you to use Ruby’s inheritance mechanism to easily factor out commonality among several related views. Using inheritance is one of the most powerful features of Fortitude.

What We’re Trying To Do

Imagine we’re building the initial phases of a social network, and the time has come to create our main social feed. This feed is going to list various events: friends who have accepted our friend request, new posts in groups we follow, and even new features we’ve introduced to the site.

This presents an interesting challenge for building views. The views for these feed items are likely to have quite a bit in common — yet almost all of it is going to be customized at some point. Still, let’s start simply, with just the three feed items mentioned above.

First, the “accepted friend request” view:

app/views/feed/items/accepted_friend_request.html.erb
<div class="feed_item feed_item_accepted_friend_request" id="feed_item_<%= item.id %>">
  <div class="feed_item_header">
    <%= image_tag 'new_friend.png', :class => 'feed_item_image' %>
    <h5><strong><%= item.accepting_user.first_name %></strong> accepted your friend request!</h5>
  </div>

  <div class="feed_item_body">
    <p>
      <a href="<%= profile_url(item.accepting_user) %>">
        <img src="<%= item.accepting_user.profile_image %>">
        <strong><%= item.accepting_user.full_name %></strong> is now your friend!
      </a>
    </p>
    <p>You should say hello! It’s only <%= ((Time.now - item.accepting_user.date_of_birth) / 1.day) %> days until their birthday!</p>
  </div>

  <div class="feed_item_footer">Posted at <%= item.created_at %></div>
</div>

Now, the “new posts in groups you follow” view:

app/views/feed/items/new_group_posts.html.erb
<div class="feed_item feed_item_new_posts" id="feed_item_<%= item.id %>">
  <div class="feed_item_header">
    <%= image_tag 'new_post.png', :class => 'feed_item_image' %>
    <h5>New posts in <%= item.new_groups.length %> groups!</h5>
  </div>

  <div class="feed_item_body">
    <p>There have been new posts in the following groups:
      <ul>
        <% item.new_groups.each do |group| %>
        <li><a href="<%= group_url(group) %>"><strong><%= group.title %></strong> (<%= group.new_posts %> new posts)</a></li>
        <% end %>
      </ul>
    </p>
  </div>

  <div class="feed_item_footer">Posted at <%= item.created_at %></div>
</div>

And, finally, the “new feature” view:

app/views/feed/items/new_feature.html.erb
<div class="feed_item feed_item_new_feature" item="feed_item_<%= item.id %>">
  <div class="feed_item_header">
    <%= image_tag 'features/custom_groups.png', :class => 'feed_item_image' %>
    <h3>New features have launched!</h3>
  </div>

  <div class="feed_item_body">
    <h5>We’ve been improving the site for your benefit!</h5>
    <p>You can now create custom groups! We know you’ve all been asking for this feature for a while,
       and it’s now ready for your use! <a href="<%= custom_groups_url %>">Click here</a> to create one!</p>
  </div>

  <div class="feed_item_footer">Posted at <%= item.created_at %></div>
</div>

(Please note that we’ve simplified a lot of these views for our example. For example, you’d never want to show a user a raw Ruby Time value as we’ve done here.)

We can easily notice a lot of commonality in the views above:

  • Container: All the views have a similar surrounding div that contains a per-view class and a per-feed-item id.
  • Header: Each view has a header that’s surrounded by a div that contains an image and some per-view HTML.
  • Body: Each view has a body that’s surrounded by a div that contains arbitrary HTML for that view — the real contents of our feed item.
  • Footer: Each view also has a footer that’s identical for all views.

Using Standard Templating Engines

Using traditional templating engines, what can we do here? Well, have the same tools at our disposal as always: partials and helpers. We can factor out commonality using these tools, potentially using capture to allow us to write HTML we pass into a view using ERb instead of Ruby string interpolation.

Let’s first see what this looks like without using capture, and then we’ll see how or if using capture improves things.

Using Partials and Helpers

If we do our very best with this, here’s what we can end up with. We begin with a partial that starts the header of our view:

app/views/feed/items/_feed_item_header.html.erb
<div class="feed_item feed_item_<%= outer_css_class %>" id="feed_item_<%= item.id %>">
  <div class="feed_item_header">
    <%= image_tag header_icon_image, :class => 'feed_item_image' %>

We have a really awkward situation going on between the header and the body, where we need to close the div we opened, then open another one. This seems a bit heavyweight for a partial, so let’s use a helper:

app/helpers/feed_helper.rb
  def feed_item_header_closer
    "</div><div class="feed_item_body">"
  end

Next, we have the body of the feed item, which we’ll leave to the individual view itself. Finally, we have the rest of the item template:

app/views/feed/items/_feed_item_footer.html.erb
  </div>

  <div class="feed_item_footer">Posted at <%= item.created_at %></div>
</div>

Given these partials and helpers, what do our views now look like? First, let’s look at the “accepted friend request” view:

app/views/feed/items/accepted_friend_request.html.erb
<%= render :partial => 'feed_item_header',
      :locals => { :outer_css_class => 'accepted_friend_request',
                   :item => item, :header_icon_image => 'new_friend.png' } %>
      <h5><strong><%= item.accepting_user.first_name %></strong> accepted your friend request!</h5>
<%= feed_item_header_closer %>
    <p>
      <a href="<%= profile_url(item.accepting_user) %>">
        <img src="<%= item.accepting_user.profile_image %>">
        <strong><%= item.accepting_user.full_name %></strong> is now your friend!
      </a>
    </p>
    <p>You should say hello! It’s only <%= ((Time.now - item.accepting_user.date_of_birth) / 1.day) %> days until their birthday!</p>
<%= render :partial => 'feed_item_footer', :locals => { :item => item } %>

This is our “new posts in groups you follow” view:

app/views/feed/items/new_group_posts.html.erb
<%= render :partial => 'feed_item_header',
      :locals => { :outer_css_class => 'new_posts',
                   :item => item, :header_icon_image => 'new_post.png' } %>
      <h5>New posts in <%= item.new_groups.length %> groups!</h5>
<%= feed_item_header_closer %>
    <p>There have been new posts in the following groups:
      <ul>
        <% item.new_groups.each do |group| %>
        <li><a href="<%= group_url(group) %>"><strong><%= group.title %></strong> (<%= group.new_posts %> new posts)</a></li>
        <% end %>
      </ul>
    </p>
<%= render :partial => 'feed_item_footer', :locals => { :item => item } %>

And this is our “new feature” view:

app/views/feed/items/new_feature.html.erb
<%= render :partial => 'feed_item_header',
      :locals => { :outer_css_class => 'new_feature',
                   :item => item, :header_icon_image => 'custom_groups.png' } %>
      <h3>New features have launched!</h3>
<%= feed_item_header_closer %>
    <h5>We’ve been improving the site for your benefit!</h5>
    <p>You can now create custom groups! We know you’ve all been asking for this feature for a while,
       and it’s now ready for your use! <a href="<%= custom_groups_url %>">Click here</a> to create one!</p>
<%= render :partial => 'feed_item_footer', :locals => { :item => item } %>

Is this an improvement? Yes — well, maybe. We’ve successfully removed some of the commonality, although at the price of introducing a lot of verbosity. We’ve also added some serious weirdness, like divs that get opened in one partial and closed in another — all of which is, of course, added opportunity for error. All in all, it certainly isn’t clean or clear; once again, extracting the commonality has come at a serious cost in comprehensibility and readability.

Let’s see if using capture makes this much better.

Using capture

The capture method isn’t particularly common or elegant, but it can be useful in situations like this to pass HTML into other views. If we use this, let’s see what our shared partial looks like first:

app/views/feed/items/_feed_item_base.html.erb
<div class="feed_item feed_item_<%= outer_css_class %>" id="feed_item_<%= item.id %>">
  <div class="feed_item_header">
    <%= image_tag header_icon_image, :class => 'feed_item_image' %>
    <%= header_html %>
  </div>

  <div class="feed_item_body">
    <%= body_html %>
  </div>

  <div class="feed_item_footer">Posted at <%= item.created_at %></div>
</div>

This certainly looks a lot better than our previous solution of two partials and a helper. It’s simpler, and keeps the overall structure of the partial much clearer.

Given this, what do our actual feed items look like now?

app/views/feed/items/accepted_friend_request.html.erb
<% header_html = capture do %>
  <h5><strong><%= item.accepting_user.first_name %></strong> accepted your friend request!</h5>
<% end %>
<% body_html = capture do %>
    <p>
      <a href="<%= profile_url(item.accepting_user) %>">
        <img src="<%= item.accepting_user.profile_image %>">
        <strong><%= item.accepting_user.full_name %></strong> is now your friend!
      </a>
    </p>
<p>You should say hello! It’s only <%= ((Time.now - item.accepting_user.date_of_birth) / 1.day) %> days until their birthday!</p>
<% end %>
<%= render :partial => 'feed_item_base', :locals => {
      :outer_css_class => 'accepted_friend_request',
      :item => item,
      :header_icon_image => 'new_friend.png',
      :header_html => header_html,
      :body_html => body_html
} %>
app/views/feed/items/new_group_posts.html.erb
<% header_html = capture do %>
  <h5>New posts in <%= item.new_groups.length %> groups!</h5>
<% end %>
<% body_html = capture do %>
  <p>There have been new posts in the following groups:
    <ul>
      <% item.new_groups.each do |group| %>
      <li><a href="<%= group_url(group) %>"><strong><%= group.title %></strong> (<%= group.new_posts %> new posts)</a></li>
      <% end %>
    </ul>
  </p>
<% end %>
<%= render :partial => 'feed_item_base', :locals => {
      :outer_css_class => 'new_posts',
      :item => item,
      :header_icon_image => 'new_post.png',
      :header_html => header_html,
      :body_html => body_html
} %>
app/views/feed/items/new_feature.html.erb
<% header_html = capture do %>
  <h3>New features have launched!</h3>
<% end %>
<% body_html = capture do %>
  <h5>We’ve been improving the site for your benefit!</h5>
    <p>You can now create custom groups! We know you’ve all been asking for this feature for a while,
       and it’s now ready for your use! <a href="<%= custom_groups_url %>">Click here</a> to create one!</p>
<% end %>
<%= render :partial => 'feed_item_base', :locals => {
      :outer_css_class => 'new_feature',
      :item => item,
      :header_icon_image => 'custom_groups.png',
      :header_html => header_html,
      :body_html => body_html
} %>

Alas, although we have a much nicer shared partial this time around, the views have become really verbose and quite messy. The use of capture clutters up the code and causes a lot of action-at-a-distance, and the shared partial takes enough inputs at this point that just calling it requires passing many variables that create further visual clutter.

Issues with Standard Templating Engines

Hopefully it’s not an overstatement to say, simply: ugh. We have a lot of commonality in the views we started with that it seems like we ought to be able to factor out well, and yet both major options we have available don’t do a great job. The result feels messy and inelegant.

Let’s dive in deeper and look at some of the specific issues with the results:

  • Disappearing Structure: In both refactorings, the overall structure of our views has effectively vanished — when using partials and helpers, from the shared code; when using capture, from the callers. This makes the code a lot harder to read, and, more importantly, much harder to reason about: it’s all too easy to omit a closing tag, add an extra opening tag, or just have a really hard time figuring out how and where to change something. If you wanted to create a structure prone to generating lots of bugs, this is almost exactly what you’d do.
  • Verbosity: Our refactored code is almost as long as the code it replaces in both cases, and a lot harder to read. Is this really an improvement?
  • Lack of Flexibility: Now consider in both cases what it would take to, for example, be able to customize the footer — which is common to all views now, but which you can certainly imagine being customized at some point in the near future. With partials and helpers, you suddenly have to split the shared code into another partial; with capture, it’s easier, but also involves a gross defaulting of HTML in the shared view and adds more verbosity any place it’s customized.

Using Fortitude

OK. So, how can Fortitude help? Because each Fortitude view or partial is simply a Ruby class, we can define a base view class here outlining the general structure, and with simple method calls for overridden or missing content:

app/views/feed/items/feed_item.html.rb
class Views::Feed::Items::FeedItem < Views::Base
  needs :item

  def content
    div(:class => [ "feed_item", "feed_item_#{outer_css_class}" ], :id => "feed_item_#{item.id}") {
      div(:class => 'feed_item_header') {
        image_tag header_icon_image, :class => 'feed_item_image'
        header_content
      }

      div(:class => 'feed_item_body') { body_content }

      div("Posted at #{item.created_at}", :class => 'feed_item_footer')
    }
  end

  def header_content
    raise "You must implement this method in #{self.class.name}"
  end

  def body_content
    raise "You must implement this method in #{self.class.name}"
  end

  def outer_css_class
    self.class.name.demodulize.underscore
  end
end

Our base class clearly defines the overall structure of all feed item views, and that it needs an item in order to be rendered. The header_content and body_content methods are left unimplemented, to be provided by subclasses.

We’ve also used a neat trick: because this is Ruby code, we’ve used a little bit of metaprogramming convention-over-configuration to calculate the outer_css_class, instead of having to pass it in. This both makes callers cleaner and eliminates a source of inconsistency or error, because outer_css_class can no longer differ from the name of the view itself at all. And yet, exactly because it’s a separate method, if a subclass needed to override this class, it could, extremely easily.

Given this, what do these subclasses look like? We’ll start with the “accepted friend request” view, which turns out to be the most complex one:

app/views/feed/items/accepted_friend_request.html.rb
class Views::Feed::Items::AcceptedFriendRequest < FeedItem
  def header_content
    h5 {
      strong item.accepting_user.first_name; text " accepted your friend request!"
    }
  end

  def body_content
    p {
      a(:href => profile_url(item.accepting_user)) {
        img(:src => item.accepting_user.profile_image)
        strong item.accepting_user.full_name; text " is now your friend!"
      }
    }
    p "You should say hello! It’s only #{days_until_birthday} days until their birthday!"
  end

  private
  def days_until_birthday
    (Time.now - item.accepting_user.date_of_birth) / 1.day
  end
end

From the view above, it’s hopefully immediately obvious what content AcceptedFriendRequest supplies (its two public methods), and where it goes (since they have clear names). Further, we’ve used Fortitude’s ability to add “helper methods” to just a single class to easily factor out the days_until_birthday method.

Now, let’s look at the “new group posts” view:

app/views/feed/items/new_group_posts.html.rb
class Views::Feed::Items::NewGroupPosts < FeedItem
  def header_content
    h5 "New posts in #{item.new_groups.length} groups!"
  end

  def body_content
    p {
      text "There have been new posts in the following groups:"
      ul {
        item.new_groups.each do |group|
          li {
            a(:href => group_url(group)) {
              strong group.title; text " (#{group.new_posts} new posts)"
            }
          }
        end
      }
    }
  end
end

And the “new feature” view:

app/views/feed/items/new_feature.html.rb
class Views::Feed::Items::NewFeature < FeedItem
  def header_content
    h3 "New features have launched!"
  end

  def body_content
    h5 "We’ve been improving the site for your benefit!"
    p {
      text "You can now create custom groups! We know you’ve all been asking for this feature for a while, "
      text "and it’s now ready for your use! "; a("Click here", :href => custom_groups_url); text " to create one!"
    }
  end
end

At this point, these views almost seem positively boring — they each contain exactly what you’d expect them to contain, with no surprises at all. Given that truly well-factored code often seems boring because it’s so straightforward, we’ll take that as a good thing.

The Benefits

What have we done here? Put simply, we’ve leveraged Ruby’s built-in inheritance mechanism — a mechanism every single Ruby programmer on your team already knows extremely well — to build views that are far more comprehensible and maintainable than anything we could possibly have achieved with traditional templating engines.

In many ways, the key behind Fortitude is exactly that it doesn’t invent some brand-new paradigm for writing your view code. Instead, it allows you to leverage all the techniques you already have for factoring the rest of your application, and simply lets you apply them to your views.

Imagine, for example, that you finally do need to customize that shared footer in at least one feed-item view. What do you do? Simple: extract it into a method in the base class, and override it in whichever view you need to customize it in. You can even easily call super (or not), either before, after, or in the middle of the overridden method, and it behaves exactly how you’d expect, inserting the default footer contents at exactly that point in your view.

Next, we’ll see how using Fortitude classes can allow us to create a contextual, elegant “mini-language” for building complex views very easily.