The easiest way to handle displaying bad data

I've spent time on a few different projects recently where some simple changes helped to clean up some difficult parts of our code. Here's an easy way to simplify your code.

Displaying the wrong thing

On a recent project there was some existing view code that expected to display a link to a person's profile on another site.

But there was a problem where the link just wasn't working properly. Some profiles properly linked to external sites, but some linked to a bad url.

Jim's profile might have had a link like this: http://other.site.com/profile/123

But Amy's profile had a link like this: http://mycompany.com/dashboard/other.site.com/profile/234

Do you see what's wrong there? The link code was generated like this <a href="other.site.com..."> instead of including the important http:// in it.

The problem was that our database stored either value. While we could change the validation of the data on creation, we'd still need to ensure that existing data was handled properly.

Rather than processing all of our data and updating it, presenters are a perfect solution for this.

Clean up with SimpleDelegator

I wrote about delegate.rb secrets in detail on my blog but here's a quick example of how you can use it right now to clean up some code.

Often Rails apps in particular end up with a lot of logic inside the views and this can quickly grow out of control. One way to simplify this is to create a presenter object which brings together the object you need from your controller and the view.

First, let's change the controller.

Down below, we'll go over how this can help you with more complex code, but let's keep it simple for now. Take any of your basic controllers that look like this:

def show
  @user = User.find(params[:id])
end

Change it to:

def show
  @user = User.find(params[:id])
  @presenter = UserPresenter.new(@user, view_context)
end

That's the first small step, but it gives us a lot more flexability. One thing to note is that view_context method is the controller's reference to the view that it will render. Let's walk through the next step...

Building a Presenter

Make a user_presenter.rb file somewhere in your app directory (app/models is fine, and later you may choose somewhere else but this is fine for now).

require 'delegate'
class UserPresenter < SimpleDelegator
  def initialize(object, view)
    super(object)
    @view = view
  end
end

This creates a SimpleDelegator object which wraps (in this case) the @user from our controller. Since the links we displayed were acting incorrectly, let's solve that.

First, I always want to use method names that make sense to me. SimpleDelegator provides the method __getobj__ to refer to the object that you've wrapped, but it's ugly, and you or others may forget exactly what that means. From the start, I provide an alias for myself:

class UserPresenter < SimpleDelegator
  alias_method :user, :__getobj__
end

Now I can use a method that makes things much clearer. So let's get to the link fixing...

class UserPresenter < SimpleDelegator
  def other_site_link
    if user.other_site_link.match(/\Ahttp:\/\//)
      user.other_site_link
    else
      "http://#{user.other_site_link}"
    end
  end
end

This simple change allows our presenter to handle the case where the "other_site_link" doesn't have the "http://" prefix.

Now I can go to my view and change from @user to @presenter.

After that, it's far easier to test that my presenter displays the correct information and handle when the database doesn't.

Other Benefits

This was a very small example. I've seen many controllers with more than just one instance variable to handle data for the view.

A presenter gives you a single object to handle multiple items for your view. Instead of making a new instance variable, why not make a method on your presenter?

Try it out. Often we wait too long to make a new class to handle some behavior and in the case of our views, the code easily becomes unwieldy.

Stay tuned for more updates about handling code growth and business logic. In the meantime, what difficulty are you overcoming with your code?