In a previous article I showed a way to move display code into a template object to manage missing data. Here's what the basic template code looked like:
class Template
def display_address(address)
province_and_postal_code = [address.province, address.postal_code].compact.join(' ')
province_and_postal_code = nil if province_and_postal_code.empty?
city_province_postal_code = [address.city, province_and_postal_code].compact.join(', ')
city_province_postal_code = nil if city_province_postal_code.empty?
[address.street, city_province_postal_code].compact.join("\n")
end
end
That's relatively short and easy to read code for displaying the data for an address object.
But the display_address
method contains the entire algorithm for displaying the data and stripping away any missing values. When we needed to add a new format type we created an HtmlTemplate
and it contained duplicated code for the display_address
. That reeks of a future bug where we might need a change to the algorithm and only remember to change one template type.
If we add new attributes to our address, we'd need to change every template so it could handle the new data. Inheritance is an easy solution for managing the way multiple types can handle the change.
And because we specifically allow for data to be missing, we can treat our template object like a partially applied function. Here's what our main template will have...
We'll need methods to set the values to be used for the display data, methods to handle the removal of missing values from the data to be processed, and finally the display method.
To set the data values, we can use attr_accessor
:
class Template
attr_accessor :province, :postal_code, :city, :street
def province_and_postal_code
value = [province, postal_code].compact.join(' ')
if value.empty?
nil
else
value
end
end
def city_province_postal_code
value = [city, province_and_postal_code].compact.join(', ')
if value.empty?
nil
else
value
end
end
def address_lines
[street, city_province_postal_code].compact
end
def display_address
address_lines.join("\n")
end
end
With that change, our additional template types can inherit from our beginning Template class and change the behavior relevant to the needs of its format:
class HtmlTemplate < Template
def display_address
address_lines.join("
")
end
end
Eventually we'll find that the addresses we need to handle might require a secondary bit of information like an apartment number.
class Template
# Additional attribute
attr_accessor :apartment
# Updated collection of information
def address_lines
[street, apartment, city_province_postal_code].compact
end
end
The way our Address objects interact with these templates would change, of course, but could allow the Address to make decisions about what may be revealed to the outside world:
class Address
def display(template)
unless protect_privacy?
template.street = street
template.apartment = apartment
template.postal_code = postal_code
end
template.city = city
template.province = province
template.display_address
end
end
By creating a standard set of template methods, we can treat our objects containing data separately from the objects which display them. What else could we do now that we've made this distiction? For example, what might a template for an IO stream look like?