Map as a Presenter pattern and more

gmarik 3 min
Table Of Contents ↓

What are the responsibilities of a Presenter?

The presenter acts upon the model and the view. It retrieves data from repositories (the model), and formats it for display in the view

or briefly

transform data from one to another form(representation)

I like to think of Presenter as just a transformation function withing a context(an MVC).

Thinking with mapping

Map is a very simple but powerful concept. Rubyists (and other functional style ppl) use it often to transform data:

[1, 2, 3].map {|e| '!' * e }
=> ['!', '!!', '!!!']

But how it relates to Presenter?

Well, Presenter is a Map too.

Let’s take an example from rails tips

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

  def as_json()
    {
      'id'          => @user.id,
      'email'       => @user.email,
      'name'        => @user.name,
      'first_name'  => @user.first_name,
      'last_name'   => @user.last_name,
      'urls'        => {
        'self'    => "#{Gauges.api_url}/me",
        'gauges'  => "#{Gauges.api_url}/gauges",
        'clients' => "#{Gauges.api_url}/clients",
      }
    }
  end
end

and used like this:

get('/me') do
  content_type(:json)
  {:user => UserPresenter.new(current_user)}.to_json
end

rewriting it Map-style would look like:

UserPresenter = lambda do |user|
  {
    'id'          => user.id,
    'email'       => user.email,
    'name'        => user.name,
    'first_name'  => user.first_name,
    'last_name'   => user.last_name,
    'urls'        => {
      'self'    => "#{Gauges.api_url}/me",
      'gauges'  => "#{Gauges.api_url}/gauges",
      'clients' => "#{Gauges.api_url}/clients",
    }
  }
end

and use:

get('/me') do
  content_type(:json)
  {:user => UserPresenter[current_user]}.to_json
end

Big deal you say?!

Well lets use it for bunch of users

get('/all') do
  content_type(:json)
  {:users => User.all.map(&UserPresenter) }.to_json
end

nicer than:

get('/all') do
  content_type(:json)
  {:users => User.all.map {|u| UserPresenter.new(u).as_json } }.to_json
end

less code #FTW ( Of course you can tweak original class to act as Proc, but it requires more code )

Single responsibility and re-usability

Thinking with maps means having reusable “functions” with predefined interface

UrlsPresenter = lambda do
  {
    'self'    => "#{Gauges.api_url}/me",
    'gauges'  => "#{Gauges.api_url}/gauges",
    'clients' => "#{Gauges.api_url}/clients",
  }
end

UserPresenter = lambda do |user|
  {
    'id'          => user.id,
    'email'       => user.email,
    'name'        => user.name,
    'urls'        => UrlsPresenter.call
  }
end

is also a good example of Single Responsibility

Map as a Decorator

EmailFilter = lambda do |data|
  data.except(:email)
end
EmailFilter[UserPresenter[current_user]]

Map is a bidirectional concept

Along with converting output streams maps can be used to convert input streams(relative to an app’s point of view), which makes it not be a Presenter anymore:

xml = Nokogiri::XML(external_response)
# ...

XmlLineItemMap = lambda do |li_xml|
  #...
end

XmlOrderMap = lambda do |order_xml|
  {
    :order_id         => order_xml.at_css('order order_id').text,
    :order_number     => order_xml.at_css('order order_number').text,
    :order_status     => order_xml.at_css('order order_status').text,
    :total_price      => order_xml.at_css('order total_price').text,

    :line_items       => order_xml.at_css('order line_items').map(&XmlLineItemMap)
  }
end

Pros and Cons

Pros

  1. Simplicity
  2. Single Responsibility
  3. Unified interface
  4. Reusability

Also

  1. Less code: constructor was just 3 useless lines
  2. Works well on “separation boundaries” when it’s neccessary to turn objects into PORO or other way around

Cons

  1. Eagerness

Usage

I’ve been using to avoid all the json building DSLs

Note to self

Software design patterns are about responsibilities. So don’t take patterns literally. And think in interfaces.

Related Posts
Read More
Vim Script Best Practices
Gem Rails Like Sinatra Talk
Comments
read or add one↓