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.
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.
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.
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