March 20, 2015

Cleaning up your views with Simple Delegators

cover

While working on my pet project for matching tennis partners I decided to make a match history page. It displays a player's recent matches and some information pertaining to them: who played when, who won, and how much that win affected their score on the site. I also wanted each match history log to be separated by color-coded boxes.

This was the end result:
Match-History-Image

The first solution I came up with used a bunch of if statements and some messy view logic:

# (most of the view omitted)

<% @matches.each do |match| %>
  <div class=<% match.winner == current_user ? "winning_class" : "losing_class" %>>
    <div><%= match.challenger %> vs <%= match.defender %></div>
    <div><%= match.match_at %></div>
    <% if match.winner == current_user %>
      <div>+ <%= match.elo_difference></div>
    <% else %>
      <div>- <%= match.elo_difference></div>
    <% end %>
    <%= link_to "Match Details", match %>
  </div>
<% end %>

But we're doing Ruby, and this is some ugly Ruby. There's a lot of logic in the views... and a ternary? Am I insane?

Enter Simple Delegators:

A concrete implementation of Delegator, this class provides the means to delegate all supported method calls to the object passed into the constructor and even to change the object being delegated to at a later time with #__setobj__.

Using this, we can make decorators to clean up our views.

Let's start with the controller:

def index
  @matches = Match.all.map do |match|
    if match.winner == current_user
      WinningMatch.new(match)
    else
      LosingMatch.new(match)
    end
  end
end

Then we can make these decorated matches:

class WinningMatch < SimpleDelegator
  attr_reader :match

  def initialize(match)
    @match = match
    super(@match)
  end

  def match_outcome_class
    "match-stats-winner"
  end

  def elo_delta
    "+ #{match.elo_delta}"
  end

  def win_or_lose
    "won"
  end
end

We would do exactly the same for LosingMatch.

We end up with an array of matches in our #index action. Each match could either be a LosingMatch or a WinningMatch. Through the magic of duck typing, we can call the same method on either WinningMatch or LosingMatch and they'll know what to return.

So, in the end, we can create a partial like this:

<li>
  <div>
    <p><%= match.challenger_username %> vs. <%= match.defender_username %></p>
    <p><%= match.match_at.strftime("%F") %></p>
    <p><%= match.win_or_lose %></p>
    <p><%= match.elo_delta %> </p>
    <p><%= link_to "Match Details", match %></p>
  </div>
</li>

This is pretty cool! We were able to take out the various if statements and employ SimpleDelegators and duck typing to create a clean partial with absolutely no branching. I'd call that an enormous win.

Written by Elijah Kim

Roborooter.com © 2024
Powered by ⚡️ and 🤖.