In Progress
Unit 1, Lesson 21
In Progress

Display Builder

Video transcript & code

Suppose we're building an admin system for a software-as-a-service company. We're working on the part that displays customer accounts.

There are a few different kinds of account in the system. There are individual accounts with a first name, last name, and email address. There are corporate accounts, which have a company name, an email address, and a tax ID number. And there are trial accounts, which only have an email address.

PersonalAccount = Struct.new(
  :first_name,
  :last_name,
  :email)                             
CorporateAccount = Struct.new(
  :company_name,
  :email,
  :tax_id)
TrialAccount = Struct.new(:email)

We want to be able to display these users in various ways. We consider giving each class its own display methods for different formats. For instance, the #to_csv method could output comma-separated values. A #to_summary method could generate text showing just the most essential info about the account. And a #to_html could generate semantic markup representing the account. Notice that in this HTML representation, the order of fields is switched around from the other views so that the email address comes first.

require 'csv'

class PersonalAccount
  def to_csv
    CSV.generate { |csv|
      [:first_name, :last_name, :email].each do |name|
        csv << [name, self[name]]
      end
    }
  end

  def to_summary
    "#{first_name} #{last_name} <#{email}>"
  end

  def to_html
    <<"END"
<div class="account vcard">
  <p>
    Account details for:
    <span class="email">#{email}</span>
  </p>
  <p class="fn">#{first_name} #{last_name}</p>
</div>
END
  end
end
pa = PersonalAccount.new("Tom", "Servo", "tservo@example.org")
puts "CSV:"
puts pa.to_csv    

puts "\nSummary:"
puts pa.to_summary

puts "\nHTML:"
puts pa.to_html

Looking at this, we get a sinking feeling. Displaying these objects seems like it ought to be a separate and distinct responsibility from representing accounts in the system. And we can easily imagine writing an ever-expanding number of these methods in the future, as the number of account types and output formats increases.

Telling objects to convert themselves to a given format is simple and straightforward, but it doesn't scale very well. And we can easily imagine writing an ever-expanding number of these methods in future, as the number of account types and formats increases.

./conversion.png

We decide to experiment with a template approach instead. In this version, we separate the displaying out into a separate object. This object fills in the holes in a template with values from an Account object.

require 'erb'



class HtmlPersonalAccountView
  TEMPLATE = ERB.new(<<END)
<div class="account vcard">
  <p>
    Account details for:
    <span class="email"><%= email %></span>
  </p>
  <p class="fn"><%= first_name %><%= last_name %></p>
</div>
END

  def render(account)
    account.instance_eval do 
      TEMPLATE.run(binding) 
    end
  end

end

puts HtmlPersonalAccountView.new.render(pa)

This approach splits the responsibility for rendering different views of our business objects into separate classes. But the resulting templates are very tightly bound to a specific class of account. If we tried to pass a CorporateAccount into a PersonalAccount view, it would blow up because it doesn't have first_name or last_name fields.

This "View Template" strategy has the templates pulling data from the business models, making them tightly coupled to the structure and interface of those models.

./template.png

Let's explore a different approach to the problem of displaying business objects. We'll start with the PersonalAccount class again.

We add a #display method. This takes a single argument, r, which is short for renderer. We will send messages to this "renderer" object to build up a representation of the account piece by piece. We start with the name, for which we concatenate the first name and last name. Then comes the email address and the mailing address.

We add similar #display methods to the other account classes.

Notice how there doesn't have to be a one-to-one correspondence between object attributes and renderer methods. For instance, neither PersonalAccount nor CorporateAccount have #name attributes. But they both send the #name method to the renderer, one passing first name and last name, the other passing the company name. CorporateAccount is the only type that supplies a tax_id. And TrialAccount provides only a placeholder name and an email address.

class PersonalAccount
  def display(r)
    r.name("#{first_name} #{last_name}")
    r.email(email)
  end
end

class CorporateAccount
  def display(r)
    r.name(company_name)
    r.email(email)
    r.tax_id(tax_id)
  end
end

class TrialAccount
  def display(r)
    r.name("Trial Account User")
    r.email(email)
  end
end

Now let's write some renderers. We'll start with a simple CSV renderer. This one treats every one-argument message sent to it as a name/value pair.

class CsvRenderer
  def initialize(destination)
    @csv = CSV.new(destination)
  end

  def method_missing(name, value=nil)
    @csv << [name, value]
  end
end

We can instantiate this renderer, with $stdout as its destination, pass it into one of our Account types, and get CSV output immediately.

ca = CorporateAccount.new(
  "Yoyodyne", 
  "john@example.org")
ta = TrialAccount.new("crooooow@example.org")
renderer = CsvRenderer.new($stdout)

puts "Personal Account:"
pa.display(renderer)

puts "\nCorporate Account:"
ca.display(renderer)

puts "\nTrial Account:"
ta.display(renderer)

We'd also like to be able to generate HTML output. For that, we create a new renderer. This one also defines #method_missing. This time around, it sets instance variables based on the messages and values sent in. It also defines rendering methods which use here-documents and string interpolation to construct HTML.

class HtmlAccountRenderer
  def method_missing(name, value=nil)
    instance_variable_set("@#{name}", value)
  end

  def render
    <<"END"
<div class="account vcard">
  <p>
    Account details for:
    <span class="email">#{@email}</span>
  </p>
  <p class="fn">#{@name}</p>
</div>
END
  end
end

To use this renderer, we instantiate it, pass it into an account's #display method, and then finalize the output by sending the #render message. Note that whereas the CSV renderer streamed output as soon as it received more data, this renderer collects data and then renders when it is finished, so that it can control the order of information in the resulting HTML.

puts "Personal Account:"
renderer = HtmlAccountRenderer.new
pa.display(renderer)
puts renderer.render

puts "\nCorporate Account:"
renderer = HtmlAccountRenderer.new
ca.display(renderer)
puts renderer.render

puts "\nTrial Account:"
renderer = HtmlAccountRenderer.new
ta.display(renderer)
puts renderer.render

Finally, we create a SummaryRenderer. This one ignores most messages, and just saves the name and email for rendering.

class SummaryAccountRenderer
  def method_missing(name, value=nil)
    # NOOP
  end

  def name(name)
    @name = name
  end

  def email(email)
    @email = email
  end

  def render
    "#{@name} <#{@email}>"
  end
end
puts "Personal Account:"
renderer = SummaryAccountRenderer.new
pa.display(renderer)
puts renderer.render

puts "\nCorporate Account:"
renderer = SummaryAccountRenderer.new
ca.display(renderer)
puts renderer.render

puts "\nTrial Account:"
renderer = SummaryAccountRenderer.new
ta.display(renderer)
puts renderer.render

This new strategy we're using inverts the typical flow of control for displaying business models to the user. Instead of having a model object either interrogate itself, or having a view object interrogate a model, in this case the model "pushes" its relevant attributes into the renderer. There is no requirement on the model that it must respond to a list of specific attribute getter methods; instead, it translates its own names for attributes into a language that the renderer understands inside its #display method.

./renderer.png

Because the #display method is the only place the two kinds of object "touch", they are able to vary independently from each other. There can be many types of account, and many types of renderer. Renderers which don't understand or don't care about a particular attribute are free to ignore it. And models which don't possess a particular attribute are not required to answer for it. As a result changes to one or the other side of this collaboration are less likely to break the application.

This approach is an application of the Builder pattern, building up a representation piece by piece. As you may have noticed, this is also an example of "Tell, Don't Ask" in action. The model tells the renderer about its own attributes, instead of the renderer asking.

This is a pretty heavyweight pattern, the kind that probably shouldn't be the first thing you reach for. But in cases where you have many different kinds of objects, and many formats in which to display them, it may be more manageable than other alternatives.

That's plenty for today. Happy hacking!

Responses