Here's a quick tip about how I used my presenters to handle a collection of objects.
Previous posts relevant to this are:
- Ruby delegate.rb Secrets
- The easiest way to handle displaying bad data
- 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.
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) %>
- <% end %>
<%= session_presenter.title %>
<%= session_presenter.other_view_method %>
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| %>
- <% end %>
<%= session.title %>
<%= session.other_view_method %>
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
nil and providing the
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
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.