Example 2. A Larger View

Fortitude’s benefits become even more evident as your application scales. Next, we’ll take a look at a much larger view and the ways Fortitude can improve code at scale.

What We’re Trying To Do

This view has been adapted from another real-world application. It’s a view from an administrative page that shows a list of blocked users, reported users, and reporting users (i.e., those who have reported other users).

/app/views/admin/users/user_reporting.html.erb
<h1>Blocked users in the last <%= @time_span_days %> days:</h1>

<table class="blocked_users user_list">
  <thead>
    <tr>
      <th>User ID</th>
      <th>Username</th>
      <th>Email</th>
      <th>Last Login</th>
      <th>Blocked Because</th>
      <th>Blocked By</th>
    </tr>
  </thead>
  <tbody>
    <% @blocked_users.each do |blocked_user|
       blocked_details = @blocked_user_reasons[blocked_user.id] %>
      <tr>
        <td><%= blocked_user.id %></td>
        <td><%= blocked_user.username %></td>
        <td><%= blocked_user.email || "<span class="no_email">(none available)</span>".html_safe %></td>
        <td><%= time_ago_in_words(blocked_user.last_login_at) %></td>
        <td><%= blocked_details[:reason] %></td>
        <td><%= blocked_details[:admin_user] %></td>
      </tr>
  </tbody>
</table>

<h1>Reported Users in the last <%= @time_span_days %> days:</h1>
<table class="reported_users user_list">
  <thead>
    <tr>
      <th>User ID</th>
      <th>Username</th>
      <th>Email</th>
      <th>Last Login</th>
      <th>Reported For</th>
      <th>Report Text</th>
      <th>Reported By</th>
    </tr>
  </thead>
  <tbody>
    <% @reported_users.each do |reported_user|
       report_details = @reported_user_reasons[reported_user.id] %>
      <tr>
        <td><%= reported_user.id %></td>
        <td><%= reported_user.username %></td>
        <td><%= reported_user.email || "<span class="no_email">(none available)</span>".html_safe %></td>
        <td><%= time_ago_in_words(reported_user.last_login_at) %></td>
        <td><%= report_details[:abuse_flag] %></td>
        <td><%= report_details[:abuse_text] %></td>
        <td><%= report_details[:abuse_reporter] %></td>
      </tr>
  </tbody>
</table>

<h1>Most Reporting Users in the last <%= @time_span_days %> days:</h1>
<table class="reporting_users user_list">
  <thead>
    <tr>
      <th>User ID</th>
      <th>Username</th>
      <th>Email</th>
      <th>Last Login</th>
      <th>Total Reported Users</th>
      <th>Reported-For Breakdown</th>
    </tr>
  </thead>
  <tbody>
    <% @top_reporting_users.each do |reporting_user|
       reporting_details = @top_reporting_user_details[reporting_user.id] %>
      <tr>
        <td><%= reporting_user.id %></td>
        <td><%= reporting_user.username %></td>
        <td><%= reporting_user.email || "<span class="no_email">(none available)</span>".html_safe %></td>
        <td><%= time_ago_in_words(reporting_user.last_login_at) %></td>
        <td><%= reporting_details[:flags].uniq { |f| f.reported_user_id }.length %></td>
        <td>
          <% grouped_by_flag = reporting_details[:flags].group_by { |f| f.flag_type }
             grouped_by_flag.each do |flag_type, flags| %>
            Flag <strong><%= flag_type %></strong>: <%= flags.length %> total flag(s)<br>
          <% end %>
        </td>
      </tr>
  </tbody>
</table>

Obviously, the most notable thing about this view is that it’s big. That’s OK — there’s no inherent reason that long views are bad. The tables in this view aren’t used anywhere else, so there’s no need to share the code with anything else.

However, the length of this view makes it pretty hard to read; if you open it on anything but a huge monitor, it’s not even obvious that there is a list of reporting users, for example.

Further, there’s a lot of duplication among these three tables, which would be nice to clean up, too.

Using Standard Templating Engines

Using standard templating engines, our options in this case are pretty limited.

Splitting Apart the View

One thing we could do would be to break out the three tables into their own partials. This does work, and looks pretty concise:

/app/views/admin/users/user_reporting.html.erb
<%= render :partial => 'blocked_users' %>
<%= render :partial => 'reported_users' %>
<%= render :partial => 'reporting_users' %>

However, we’ve now created a subtle but insidious problem: these three new partials now require quite a few variables to be set in order to render them — specifically, @time_span_days, @blocked_users, and @blocked_user_reasons for one of them, @time_span_days, @reported_users, and @reported_user_reasons for the next, and so on. This is fine, except that nowhere in the code can you tell this— if you look at the original view, it now doesn’t mention those whatsoever, and you can only tell that the partials require those variables by carefully scouring their text to see what they use.

Even worse, nowhere in the originating view are any of these variables mentioned! If you were to look at the view, it’s literally impossible to tell which variables the controller needs to set in order to make it work. You need to go look at the partials it renders. Now, imagine we perform some further refactorings — you might need to look through several layers of partials, carefully writing down which @ variables are used and deduplicating them, just to wrap your head around the data being communicated from the controller to the view.

Although many views are written this way, this is not any better in view code than it is in any other code. We’re passing data using what are effectively implicit global variables, and that’s seriously detrimental to maintainability and readability in any application.

Further: if we do this, we haven’t done anything to factor out the commonality of these tables…and, because they’re now in three separate files, the likelihood that they’ll diverge in a bad way goes up. (For example, it’d be much easier for a developer to add a new common column to one of them, while forgetting to add it to the others — and now we have not just duplication, but duplication and inconsistency, which is pretty close to the canonical definition of poorly-factored code.)

Unifying the Tables

Trying to unify the code for the three tables is a lot trickier. While the tables actually have a great deal of commonality, they also have varying numbers of columns, and that makes it considerably harder. We have a few choices here:

  1. We could create one partial for the entire table, and then pass in two arrays: one of the extra column names, and the second an array of arrays of HTML fragments, one outer array for each row, one element in the inner array for each column. Trying to even write that calling code seems like an exercise in sheer mess, and prospects for maintainability are slim at best.
  2. We could create a partial for the table header, and one for the body, and pass in the above data, structured just as mentioned above; this cleans it up a little more, but doesn’t really make any real fundamental difference, since the big mess will be in the caller, not the partials.
  3. We could create a partial for the table header, one for the first chunk of the table body, and then one that renders a single row of the body, passing in HTML for the cells, and loop over the body partial.

This last option certainly seems like the best. Let’s see what it looks like in practice. First, the header partial:

/app/views/admin/users/_user_reporting_table_header.html.erb
<h1><%= title %></h1>

<table class="<%= table_classes %> user_list">
  <thead>
    <tr>
      <th>User ID</th>
      <th>Username</th>
      <th>Email</th>
      <th>Last Login</th>
      <% table_header_columns.each do |table_header_column| %>
      <th><%= table_header_column %></th>
      <% end %>
    </tr>
  </thead>
  <tbody>

And the body-row partial:

/app/views/admin/users/_user_reporting_table_row_begin.html.erb
<tr>
  <td><%= user.id %></td>
  <td><%= user.username %></td>
  <td><%= user.email || "<span class="no_email">(none available)</span>".html_safe %></td>
  <td><%= time_ago_in_words(user.last_login_at) %></td>
  <%= extra_columns %>
</tr>

Finally, here’s the caller:

/app/views/admin/users/user_reporting.html.erb
<%= render :partial => 'user_reporting_table_header', :locals => {
  :title => "Blocked users in the last #{time_span_days} days:",
  :table_classes => "blocked_users",
  :table_header_columns => [ "Blocked Because", "Blocked By" ]
} %>
  <tbody>
<% @blocked_users.each do |blocked_user|
  blocked_details = @blocked_user_reasons[blocked_user.id] %>
  <%= render :partial => 'user_reporting_table_row_begin', :locals => {
    :user => blocked_user, :extra_columns => "<td>#{h(blocked_details[:reason])}</td><td>#{h(blocked_details[:admin_user])}</td>"
  } %>
<% end %>
  </tbody>
</table>

<%= render :partial => 'user_reporting_table_header', :locals => {
  :title => "Reported users in the last #{time_span_days} days:",
  :table_classes => "reported_users",
  :table_header_columns => [ "Reported For", "Report Text", "Reported By" ]
} %>
  <tbody>
<% @reported_users.each do |reported_user|
    report_details = @reported_user_reasons[reported_user.id] %>
  <%= render :partial => 'user_reporting_table_row_begin', :locals => {
    :user => reported_user, :extra_columns =>
      "<td>#{h(report_details[:abuse_flag])}</td><td>#{h(report_details[:abuse_text])}</td><td>#{h(report_details[:abuse_reporter])}</td>"
  } %>
<% end %>
  </tbody>
</table>

<%= render :partial => 'user_reporting_table_header', :locals => {
  :title => "Most Reporting Users in the last #{time_span_days} days:",
  :table_classes => "reporting_users",
  :table_header_columns => [ "Total Reported Users", "Reported-For Breakdown" ]
} %>
  <tbody>
<% @top_reporting_users.each do |reporting_user|
    reporting_details = @top_reporting_user_details[reporting_user.id]
    total_flags = reporting_details[:flags].uniq { |f| f.reported_user_id }.length

    grouped_by_data = grouped_by_flag.map do |flag_type, flags|
      capture do %>
      Flag <strong><%= flag_type %></strong>: <%= flags.length %> total flag(s)<br>
    <% end
    end.join("
") %>

  <%= render :partial => 'user_reporting_table_row_begin', :locals => {
    :user => reporting_user, :extra_columns =>
      "<td>#{h(total_flags)}</td><td>#{grouped_by_data}</td>"
  } %>
<% end %>
  </tbody>
</table>

Issues with Standard Templating Engines

Ugh. We’ve managed to unify the tables, and yet only at significant expense. Let’s try to break down the issues we see here:

  • Caller Mess: By far the most apparent issue here: now our caller is almost inscrutable. It doesn’t even look at first glance like it has tables in it, the actual HTML involved is strewn in little tiny chunks that are difficult to find (and often is now embedded as Ruby strings, not ERb HTML), we’ve had to resort to capture to pass HTML in, and so on. Generally speaking, it sure isn’t very readable.
  • Dangerous (XSS Potential): Once again, we’re back in the land of having to be very, very careful about HTML escaping. We have to call #h manually around the user data we put into the extra_columns partial parameter. And if we forget, even once? We now have an XSS vulnerability.
  • Falling back to #capture: For our last table, building the string we need to pass in as extra_columns requires enough logic that we either would have to use Ruby string concatenation multiple times, or, better, use #capture like we do here. While there’s nothing fundamentally wrong with #capture, the way it suddenly builds HTML out-of-order almost always makes code harder to read and harder to maintain and debug.
  • File Clutter: Our single view is now represented using three files, all of which properly live in the app/views/admin/users directory. Initially, this isn’t that big a deal; however, in a large application, this really adds up. Add seven more views like this, and now you have 24 separate view files or partials in that directory, with no obvious way of knowing which partials belong to which views (or even knowing for sure which partials might not even be used any more). In other words, the fact that you have to create a new file just to be able to reuse some code, even within a single view, is a heavyweight restriction that leads to more files than you otherwise might want.
  • Indentation (of Source): This is one of those issues that can seem minor, but really adds up over the course of a larger application. How do we indent lines in the above example? It’s not entirely clear, and there really isn’t a great solution. Basically, the natural indentation of the HTML itself is fighting with the indentation of our interlaced Ruby code. There are many different ways to handle this, but none of them are great, and none of them are accepted as “the right way”; every developer or team will have its own preferences, and your editor probably won’t be of much help.
  • Indentation (of Output): Related to this, the HTML output we get from this is now really, really poorly indented. For example, if the last table has one row, it is now going to look like this (with some sections elided for clarity):

    <html>
      <head>
        ...
      </head>
      <body>
      ...
    <h1>Most Reporting Users in the last 30 days:</h1>
    
    <table class="reporting_users user_list">
      <thead>
        <tr>
          <th>User ID</th>
          <th>Username</th>
          <th>Email</th>
          <th>Last Login</th>
          <th>Total Reported Users</th>
          <th>Reported-For Breakdown</th>
        </tr>
      </thead>
      <tbody>
    <tr>
      <td>12345</td>
      <td>jane_doe_1</td>
      <td>[email protected]</td>
      <td>about 2 hours ago</td>
      <td>17</td><td>Flag <strong>Spam</strong>: 27 total flag(s)<br>
    Flag <strong>Personal Attack</strong>: 17 total flag(s)<br>
    Flag <strong>Off-Topic</strong>: 9 total flag(s)<br>
    </td>
    </tr>
      </tbody>
    </table>
    ...
      </body>
    </html>
    

    Is this the end of the world? Of course not.

    Does it make our HTML considerably harder to read? Of course.

    Should we have to put up with this as just a fact of life in 2015? Absolutely not.

Using Fortitude

Before Refactoring, a Translation

Fortitude can help improve this situation a lot. However, before we refactor this, let’s take a look at what a simple Fortitude translation of the original, un-refactored view looks like in Fortitude:

/app/views/admin/users/user_reporting.html.rb
class Views::Admin::Users::UserReporting < Views::Base
  needs :time_span_days
  needs :blocked_users, :blocked_user_reasons
  needs :reported_users, :reported_user_reasons
  needs :top_reporting_users, :top_reporting_user_details

  def content
    h1 "Blocked users in the last #{time_span_days} days: "

    table(:class => "blocked_users user_list") {
      thead {
        tr {
          th "User ID"
          th "Username"
          th "Email"
          th "Last Login"
          th "Blocked Because"
          th "Blocked By"
        }
      }

      tbody {
        blocked_users.each do |blocked_user|
          blocked_details = blocked_user_reasons[blocked_user.id]
          tr {
            td blocked_user.id
            td blocked_user.username
            td {
              if blocked_user.email
                text blocked_user.email
              else
                span "(none available)", :class => :no_email
              end
            }
            td time_ago_in_words(blocked_user.last_login_at)
            td blocked_details[:reason]
            td blocked_details[:admin_user]
          }
        end
      }
    }

    h1 "Reported Users in the last #{time_span_days} days:"

    table(:class => "reported_users user_list") {
      thead {
        tr {
          th "User ID"
          th "Username"
          th "Email"
          th "Last Login"
          th "Reported For"
          th "Report Text"
          th "Reported By"
        }
      }

      tbody {
        reported_users.each do |reported_user|
          report_details = reported_user_reasons[reported_user.id]
          tr {
            td reported_user.id
            td reported_user.username
            td {
              if reported_user.email
                text reported_user.email
              else
                span "(none available)", :class => :no_email
              end
            }
            td time_ago_in_words(reported_user.last_login_at)
            td report_details[:abuse_flag]
            td report_details[:abuse_text]
            td report_details[:abuse_reporter]
          }
        end
      }
    }

    h1 "Most Reporting Users in the last #{time_span_days} days:"

    table(:class => "reporting_users user_list") {
      thead {
        tr {
          th "User ID"
          th "Username"
          th "Email"
          th "Last Login"
          th "Total Reported Users"
          th "Reported-For Breakdown"
        }
      }

      tbody {
        top_reporting_users.each do |reporting_user|
          reporting_details = top_reporting_user_details[reporting_user.id]
          tr {
            td reporting_user.id
            td reporting_user.username
            td {
              if reporting_user.email
                text reporting_user.email
              else
                span "(none available)", :class => :no_email
              end
            }
            td time_ago_in_words(reporting_user.last_login_at)
            td(reporting_details[:flags].uniq { |f| f.reported_user_id }.length)
            td {
              grouped_by_flag = reporting_details[:flags].group_by { |f| f.flag_type }
              grouped_by_flag.each do |flag_type, flags|
                text "Flag"; strong flag_type; text ": #{flags.length} total flag(s)"; br
              end
            }
          }
        end
      }
    }
  end
end

Although we really haven’t used the true advantages of Fortitude here, because we haven’t refactored anything yet, we can already see a few nice things about this:

  • Parameter Declarations: The needs declarations at the top of the Fortitude class show you, at a glance, exactly what you have to pass into this view to render it. Every single Fortitude view or partial has this, and it’s guaranteed to be correct: if you don’t declare something as a need, you’ll be unable to render it in the view. (You can also supply defaults to make these optional, but we’re getting ahead of ourselves.)
  • Logic vs. Rendering: Although a common convention in Rails code is to use braces for single-line blocks and do...end for multiline blocks, the convention is different in Fortitude code, and for good reason. In Fortitude code, we use braces to delimit actual HTML element contents, and do...end to delimit Ruby logic. This lets the eye scan the class easily and see exactly where there’s logic and where there’s HTML.
  • Cleanliness: This is, of course, a matter of personal preference, and not all people will feel the same way. However, to your author’s eye, the lack of all of the ERb <%= ... %> symbols cluttering up any place there’s logic in the view makes it quite a bit cleaner to read.

However, again, the syntax isn’t really the point of Fortitude — the real point is the refactoring that the syntax allows you to do. Let’s take a look at that next.

A Beautifully-Refactored View

Let’s just look at the fully-refactored view right off the bat and see what we’ve been able to do:

/app/views/admin/users/user_reporting.html.rb
class Views::Admin::Users::UserReporting < Views::Base
  needs :time_span_days
  needs :blocked_users, :blocked_user_reasons
  needs :reported_users, :reported_user_reasons
  needs :top_reporting_users, :top_reporting_user_details

  def content
    blocked_users_table
    reported_users_table
    reporting_users_table
  end

  def blocked_users_table
    users_table("Blocked Users", :blocked_users, [ "Blocked Because", "Blocked By" ], blocked_users) do |user|
      blocked_details = blocked_user_reasons[user.id]
      td blocked_details[:reason]
      td blocked_details[:admin_user]
    end
  end

  def reported_users_table
    users_table("Reported Users", :reported_users, [ "Reported For", "Report Text", "Reported By" ], reported_users) do |user|
      report_details = reported_user_reasons[user.id]
      td report_details[:abuse_flag]
      td report_details[:abuse_text]
      td report_details[:abuse_reporter]
    end
  end

  def reporting_users_table
    users_table("Most Reporting Users", :reporting_users, [ "Total Reported Users", "Reported-For Breakdown" ], top_reporting_users) do |user|
      reporting_details = top_reporting_user_details[user.id]

      td(reporting_details[:flags].uniq { |f| f.reported_user_id }.length)
      td {
        grouped_by_flag = reporting_details[:flags].group_by { |f| f.flag_type }
        grouped_by_flag.each do |flag_type, flags|
          text "Flag"; strong flag_type; text ": #{flags.length} total flag(s)"; br
        end
      }
    end
  end

  def users_table(title_prefix, css_class, extra_columns, users)
    h1 "#{title_prefix} in the last #{time_span_days} days: "

    table(:class => "#{css_class} user_list") {
      thead {
        tr {
          ([ "User ID", "Username", "Email", "Last Login" ] + extra_columns).each do |header|
            th header
          end
        }
      }

      tbody {
        users.each do |user|
          tr {
            td user.id
            td user.username
            td {
              if user.email
                text user.email
              else
                span "(none available)", :class => :no_email
              end
            }
            td time_ago_in_words(user.last_login_at)

            yield user
          }
        end
      }
    }
  end
end

The Benefits

This is such an enormous improvement over either the original ERb view or the refactored ERb view that it’s hard to know where to begin. Let’s try enumerating the advantages, over and above the ones we mentioned above that are inherent to any Fortitude code:

  • Readability of Overall Structure: The overall organization of this view is instantly obvious with one glance at the #content method (the entry point of every Fortitude view or partial): it contains a table of blocked users, a table of reported users, and a table of reporting users.
  • Readability of the Table: Our table is now in just one method. It’s completely clear what the structure of that table is, exactly how it works, and exactly where callers can pass in data or code that might change it.
  • Variation in Each Table: Similarly, it’s easy to see exactly how each table differs from the others: it’s exactly the code present in the method (e.g., #blocked_users_table, #reported_users_table, and so on).
  • Lack of File Clutter: Our view is now one single filerefactor our code is completely independent of the number of files we create for it, allowing us to keep like code together. If we have eight views in this controller, we’ll have just eight files in app/views/admin/users, and it’s crystal-clear exactly what each one is for. (Of course, you are more than welcome to factor out Fortitude code into separate files if you want — it just isn’t required in the same way it is with ERb, HAML, or other traditional templating engines.)
  • Consistent: At no point do we build HTML by interpolating Ruby strings; it’s all just Fortitude code.
  • Safe: We never have to call #h anywhere here, nor do we even have to think about HTML escaping. It’s all just handled for us, as it should be, with no risk of XSS attacks. (This doesn’t mean it’s impossible to create XSS vulnerabilities using Fortitude, of course — but it’s much, much more difficult, because you effectively never have to compose HTML using Ruby strings.)
  • Indentation (of Source): It’s obvious how the source should be indented, there’s only one reasonable way to do it, and any editor capable of indenting Ruby properly will automatically indent Fortitude properly.
  • Indentation (of Output): Because Fortitude understands the structure of your code, it always produces perfectly-formatted output. Here’s what the equivalent to the poorly-formatted HTML output from ERb above looks like if it’s coming from Fortitude:

    <html>
      <head>
        ...
      </head>
      <body>
        ...
        <h1>Most Reporting Users in the last 30 days:</h1>
    
        <table class="reporting_users user_list">
          <thead>
            <tr>
              <th>User ID</th>
              <th>Username</th>
              <th>Email</th>
              <th>Last Login</th>
              <th>Total Reported Users</th>
              <th>Reported-For Breakdown</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>12345</td>
              <td>jane_doe_1</td>
              <td>[email protected]</td>
              <td>about 2 hours ago</td>
              <td>17</td>
              <td>
                Flag <strong>Spam</strong>: 27 total flag(s)<br>
                Flag <strong>Personal Attack</strong>: 17 total flag(s)<br>
                Flag <strong>Off-Topic</strong>: 9 total flag(s)<br>
              </td>
            </tr>
          </tbody>
        </table>
        ...
      </body>
    </html>
    

    (In production, Fortitude will, by default, emit this code with no whitespace at all, reducing your page weight significantly — also something that ERb cannot do.)

Altogether, Fortitude has helped us refactor this view in a way we simply couldn’t have using any traditional templating engine.

One of the reasons the complex ERb refactoring above may seem unfamiliar to many readers is simply that the refactoring ends up being so complex that most teams simply don’t do it — they leave this view un-refactored, like the original example, since it actually is cleaner in many ways than the refactored version. And, like before, this doesn’t mean that the original is actually particularly good; it contains an awful lot of duplication — it’s just that the tools traditional templating engines give you are insufficient to refactor this code well.

By providing you all the power of Ruby to refactor your code, Fortitude allows you to refactor this code into something that’s clean, clear, and actually a joy to work with.

Next, we’ll take a look at how Fortitude lets you use inheritance to beautifully and effectively factor a set of views that all have a lot in common, but differ in their exact details.