In Progress
Unit 1, Lesson 21
In Progress

Presenter Object

“Code should have a clear separation between logic, data, and presentation.” This sounds like an obvious and straightforward piece of advice. But when we get into the thick of coding up a user interface, the lines can blur. Sometimes it’s not clear whether some logic belongs in the model or in the view layer.

Today guest chef Federico Iachetti returns to demonstrate how Presenter Objects can neatly encapsulate the gray areas between data and display. You’ll also see how presenters can help customize the presentation of data for different contexts and audiences. Enjoy!

Video transcript & code

What is a presenter?

We are writing some kind of application.

We have our product model and an instance.


User = Struct.new(:first_name, :last_name, :birth_date)

user = User.new('Federico', 'Iachetti', Time.new(1981, 12, 6))

We want to show our user on a view


view = View.new( user: user ) do
  <<~ERB
    Profile of <%= user %>

    Born on <%= user.birth_date >
  ERB
end

But when we render this view, the results don't look the way we want.

The user model doesn't define a nice #to_s method and the birth date is not in a format a human would appreciate.


puts view.render

# >> Profile of #
# >>
# >> Born on 1981-12-06 00:00:00 -0300

In order to make this prettier there are two places where we could update the code. When we display the user, we want it to say last_name, first_name. This can be easily added to the model.


User = Struct.new(:first_name, :last_name, :birth_date) do
  def to_s
    "#{last_name}, #{first_name}"
  end
end

But adding display logic to the User model overloads it with responsibilities that don't belong there. The domain models in our applications should only contain domain logic. Anything else is just a burden. And the birth date format can be changed on the view,


view = View.new( user: user ) do
  <<~ERB
    Profile of <%= user %>

    Born on <%= user.birth_date.strftime("%B %-d %Y") %>
  ERB
end

But adding logic to the view isn't ideal either. By doing so, we make it more difficult to test, since we have to render the view in order to assert states. It also violates DRY, since we'll have to scatter this logic in every view we want to use it. Another option when using a framework like Rails is to use helper methods. But helpers are globally present on all views, which makes this prone to end up with a file (or set of files) full of helpers all intended to deal with, say, names. For example, we might find a helper meant to format the name of a user; another one for the username field, which might force us to look inside to understand the difference. In another file, we might find a helper for formatting company name that uses a slightly different convention. Maybe it was written by another developer. So now we actually have to scan the entire helpers folder to find out the actual name, failing to rely on a convention. Finally, lost at the bottom of the ApplicationHelper, we might find a method called format_name, which... ...sigh... ...now we have to scan this method's implementation in order to know what it is exactly meant to do. Is it just a username formatter, a switch statement that will magically know what I'm talking about? And we've only mentioned four method names... this problem will scale quickly.


def format_user_name(name_string)
  # ...
end

def format_username(name_string)
  # ...
end

def formated_company_name(name_string)
  # ...
end

def format_name(name_string)
  # ...
end

This little example illustrates a common problem with formatting helpers: different views in the app need different formats, and as a result helper files quickly fill out with a profusion of variants. They become grab bags of methods. They're difficult to keep track of and become difficult to find the right helper for a given context. It is often hard to tell whether one of the many similar helpers is the right to use or if you need to write a new one.

The problem here is that helper files has no sense of context. Every helper for a given model is thrown into the same "bag" and is available everywhere. We can solve all of the above problems by creating an object that takes a user and it's current context and decorates it with the necessary functionality.

It has a custom to_s implementation and a properly formatted birth date.


class UserPresenter
  def initialize(user, view)
    @user = user
    @view = view
  end

  def to_s
    "#{@user.last_name}, #{@user.first_name}"
  end

  def birth_date
    @user.birth_date.strftime("%B %-d %Y")
  end
end

Now, in the view, we can replace the actual user with an instance of this new Presenter object.


view = View.new( user: user ) do
  <<~ERB
    <% user_presenter = UserPresenter.new(user, self) %>

    Profile of <%= user_presenter %>

    Born on <%= user_presenter.birth_date %>
  ERB
end

This new object is what we'll call a presenter for now on, an object that sits between the model and the view. It gives us a good place to put the methods that could be easily put on either the model or the view, but don't really belong on either one.

You might've noticed that a presenter looks a lot like a decorator. And it does, but it differs in one main way. The presenter takes two objects instead of one: the target object and the view or the context in which it needs decoration.

One of the biggest improvements we make by adding presenters to our object model is that now both the model and the view are much cleaner and deal with their own functionality.

Another great benefit of having the contextualization take place in a separate object is that now we can have different presenters for different contexts. For example, say we have our current user-facing view...


view = View.new( user: user ) do
  <<~ERB
    <% user_presenter = UserPresenter.new(user, self) %>

    Profile of <%= user_presenter %>

    Born on <%= user_presenter.birth_date %>
  ERB
end

...and also an admin dashboard which has a completely different set of expectations for displaying information. We can use a separate admin presenter to satisfy these differing requirements. This keeps the relevant formatting methods for each different view located in a small, cohesive group together.


view = View.new( user: user ) do
  <<~ERB
    <% user_presenter = UserAdminPresenter.new(user, self) %>

    Profile of <%= user_presenter %>

    Born on <%= user_presenter.birth_date %>
  ERB
end

There are further refinements we can make to the presenter concept, but that's a topic for another day.

Happy hacking!

Responses