Developmentastic

  1. Simple Presenter

    John Nunemaker inspired me in several ways this morning. He described his experience building an API for Gauges which echoes much of my own experience with the CloudApp API. Especially his third point about linking resources instead of expecting your clients to construct their own URLs. That road will only end in tears.

    Where it really hit home was his use of a presenter to provide a JSON representation of a model. I wanted to share some code that I’ve been spending some time with lately. There were a few refactorings that have made the code much easier to follow and reason about.

    There’s one main commit that shows the majority of the presenter implementation. It freed the main Sinatra app from having to concern itself with how to render a thing and now it simply asks the thing to format itself. Big difference.

    def fetch_and_render_drop(slug)
      drop = fetch_drop slug
      respond_to do |format|
    
        # Redirect to the bookmark's link, render the image view
        # for an image, or render the generic download view for
        # everything else.
        format.html do
          if drop.bookmark?
            redirect_to_api
          else
            erb drop_template(drop),
                :locals => { :drop    => drop,
                             :body_id => body_id(drop) }
          end
        end
    
        # Handle a JSON request for a **Drop**. Return the same
        # data received from the CloudApp API.
        format.json do
          Yajl::Encoder.encode drop.data
        end
      end
    end
    

    You can tell I knew the code was awful all along by the presence of comments. Compare that with today’s version.

    def fetch_and_render_drop(slug)
      drop = DropPresenter.new fetch_drop(slug), self
      respond_to do |format|
        format.html { drop.render_html }
        format.json { drop.render_json }
      end
    end
    

    I think this code’s job is pretty clear. Fetch the drop and ask for the correct representation based on the format the client requested. The DropPresenter is where the magic happens.

    class DropPresenter < SimpleDelegator
      def initialize(drop, template)
        @template = template
    
        super drop
      end
    
      def render_html
        if bookmark?
          @template.redirect_to_api
        else
          @template.erb template_name,
                        locals: { drop: self, body_id: body_id }
        end
      end
    
      def render_json
        Yajl::Encoder.encode data
      end
    
      # Some trivial private methods omitted.
    end
    

    My favorite part of this refactoring is passing a DropPresenter to existing views in place of a Drop. Everything still works and they’re none the wiser. Having this middleman in place gives the freedom to incrementally refactor logic out of the controller and views.

    For example, here’s the logic to add a logo to the page if the owner has a Free account. It’s ripe for refactoring.

    <% unless drop.subscribed? %>
      <h1><a href="http://store.getcloudapp.com/">Simple sharing</a></h1>
    <% end %>
    
    <% unless drop.subscribed? %>
      <footer id="footer">
        <h1><a href="http://store.getcloudapp.com/">Simple sharing</a></h1>
      </footer>
    <% end %>
    

    Refactoring begets refactoring.