Formatting collections of objects with SimpleDelegator

Here's a quick tip about how I used my presenters to handle a collection of objects.

Previous posts relevant to this are:

  1. Ruby delegate.rb Secrets
  2. The easiest way to handle displaying bad data
  3. Simplify your code with your own conventions

Working with collections of objects

A common problem when displaying data comes when we display collections of objects that contain collections of other objects.

I solved this for myself in a simple way with my existing presenters.

My application needed to display search results from an event management system and we called this the Agenda. Our agenda had sessions, days, and time slots and we needed a way to handle presentation details for all of them.

All of these items needed their own code, but we only needed to work with them together. From the start, we didn't break these presenters out into separate files.

Here's the structure of how we managed our agenda presenters

module AgendaBuilder
  class ResultsPresenter < ::Presenter
  end
  class DayPresenter < ::Presenter
  end
  class TimeSlotPresenter < ::Presenter
  end
  class SessionPresenter < ::Presenter
  end
end

This kept our related details together and kept us focused in one place.

Custom iterators

When in came to using these in our view templates, we didn't want to leak knowledge of the object classes into the views. From the start, a simple approach would be to initialize the presenters where you need them. Here's what it could have looked like in our ERB files:

    <% agenda.sessions.each do |session| %> <% session_presenter = AgendaBuilder::SessionPresenter.new(session, self) %>
  • <%= session_presenter.title %>

    <%= session_presenter.other_view_method %>

  • <% end %>

This is ugly. The view template has knowlegde of the classes used to implement the objects it needs to display. Instead, it would be far easier to read and handle changes if it looked like this:

    <% agenda.each_session do |session| %>
  • <%= session.title %>

    <%= session.other_view_method %>

  • <% end %>

Now that is far easier to grok.

Let's take a look at the code:

class ResultsPresenter < ::Presenter
  def each_session(&block)
    presenter = AgendaBuilder::SessionPresenter.new(nil, view)
    sessions.each do |session|
      presenter.session = session
      block.call(presenter)
    end
  end
end

This creates the each_session method which accepts a block that we use for each session in the collection.

The first part of this method may look strange: we initialize a SessionPresenter wrapping nil and providing the view object.

The presenter requires some object to initialize properly and since we're setting the session object later, we can just use nil as a placeholder. But we do this so that we can avoid creating a new presenter with each iteration of the block.

The alternative would look like this:

class ResultsPresenter < ::Presenter
  def each_session(&block)
    sessions.each do |session|
      presenter = AgendaBuilder::SessionPresenter.new(session, view)
      block.call(presenter)
    end
  end
end

While this would work, there's no need to create a new presenter object each time. Our handy session= method does the trick.

Benefits of custom iterators

Providing our own iteration method gave us the ability to change the behavior as we needed. If we merely rely on an Array with agenda.sessions.each, we're tied to that dependecy.

If we decide we don't need a SessionPresenter at all, we don't need to change our view code, we'd only need to remove that from our each_session method.

Following the pattern

We had this need for custom iterators in several places (sessions, days, and time slots) so we have an established pattern. The next step was to move this to our Presenter class so we didn't have to rewrite the same procedure each time.

The only differences between our iterators were the collection of objects (sessions, days, and time slots) and the class of the presenters needed.

All we really want to write is something like this:

class ResultsPresenter < ::Presenter
  def each_session(&block)
    wrapped_enum(AgendaBuilder::SessionPresenter, sessions, &block)
  end
end

With some minor changes to our procedure, we end up with a base wrapped_enum like this:

class Presenter < SimpleDelegator
  def wrapped_enum(presenter_class, enumerable, &block)
    presenter = presenter_class.new(nil, view)
    enumerable.each do |object|
      presenter.__setobj__(object)
      block.call(presenter)
    end
  end
end

We went back to our SimpleDelegator __setobj__ method to avoid knowledge about the domain of the presenter.

Iterating over and presenting items from our collections became so much easier and our views so much simpler. This allowed us to treat our views more like readable configuration. A title method called in the view template could be the actual title from our wrapped object just passing the data through, or, as we change our requirements, could become anything else without requiring changes to our template.

Our view template captured our intent, whereas our presenter captured the requirements and implementation.