It's the little things...

Tammer Saleh

Sometimes it’s the little things that really make coding fun.

We used to use a common pattern that helped out with our view code. By adding the link_to_xxx helpers, we could make our applications more consistent and maintainable:

def link_to_candidate(candidate, msg = nil)
  link_to(msg || h(candidate.name), candidate_path(candidate))
end

def link_to_issue(issue, msg = nil)
  link_to(msg || h(issue.title), issue_path(issue))
end

def link_to_intern(intern, msg = nil)
  link_to(msg || h(intern.name), intern_path(intern.candidate, intern))
end

#...and on...

But this grows out of hand pretty quickly—In one application we have over 30 of those puppies. Well, we figured out that by being a little clever, we could really clean this up…

def link(item, msg = nil)
  case item
  when Candidate: link_to(msg || h(item.name),  candidate_path(item))
  when Issue:     link_to(msg || h(item.title), issue_path(item))
  when Intern:    link_to(msg || h(item.name),  intern_path(item.candidate, item))
  #...
  else raise ArgumentError, "Unrecognized item given to link: #{item}"
  end
end

Well, that’s much better. It only grows one line for each model instead of four, and it’s easier to call in the views.

Candidates!
<% @candidates.each do |candidate| %>
  <%= link candidate %>
<% end %>

But it still smells a little fishy. I don’t think anyone here at thoughtbot likes seeing a case statement. Let’s get just a little more clever…

Abandon hope, all ye who enter here

def link(item, msg = nil)
  msg ||= item.send([:name, :title, :id].detect {|n| item.respond_to? n})
  method = "#{item.class.name.underscore}_path"
  link_to(msg, self.send(method, item))
end

If you’re still with me—this version of link() figures out what attribute to call on the give model and generates the xxxpath method. It’s very concise, and won’t grow with the size of your code base, but hot-damn is it a doozy to decipher. But a larger issue is that we lost our ability to handle nested resources (like `internpath(intern.candidate, intern)`).

Now, we definitely went with the case-statement version up there, but just as an exercise…

Let’s just say we required all nested models to provide a parents attribute, which returned the list of parent models. We could then clean up our link() method like so:

def link(item, msg = nil)
  msg ||= item.send([:name, :title, :id].detect {|n| item.respond_to? n})
  method = "#{item.class.name.underscore}_path"
  parents = item.parents rescue []
  link_to(msg, self.send(method, *parents, item))
end

I wonder what else could be simplified if the models could tell you what other models proceed them in the resource chain.