The problem with programming can be that there are so many ways to solve a problem. For each solution there are arguments for it and arguments against it.
In recent articles I've written about moving responsibilities into a template object and out of the objects which use them for display.
When the template code first began, its use was extremely simple:
class Address
def display(template)
template.display_address(self)
end
end
By making changes to the template to allow for shared behavior among different types of templates, the way in which our Address class used it became a bit more complex:
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
Originally the Address class knew of two features of the template, that it had a display_address
method, and that the method took a single argument intended to be the address.
After some rework, the template became easier to manage and it became easier to make alternative formats, but the changes burdened the user of the object with the need for more knowledge. The Address objects now also need to know that there are setters for street=
, apartment=
, postal_code=
, city=
, and province=
. It also needs to be implicitly aware that the template could render incomplete data; we know we aren't required to set nil values for certain attributes.
Getting back to simple
We made good changes for the template, but I want that simple interface back. I want my address to act as a value object instead of needing to keep track of passing so many arguments.
While I want to go back to this:
class Address
def display(template)
template.display_address(self)
end
end
I need a way to handle the case where we have sensitive data. What about that protect_privacy?
method?
Here's what we could do:
class Address
def display(template)
if protect_privacy?
template.display_address(private_version)
else
template.display_address(self)
end
end
def private_version
self.class.new_with_attributes(city: city, province: province)
end
end
With this change, the Address can still make a decision about displaying private data and it merely sends that version along to the template. I'm leaving the implementation of new_with_attributes
up to imagination, but we'll assume it will set the attributes we've provided on a new instance and return that.
Our template, when last we saw it, looked like this:
class Template
attr_accessor :province, :postal_code, :city, :street, :apartment
def province_and_postal_code
# ... return the combined value or nil
end
def city_province_postal_code
# ... return the combined value or nil
end
def address_lines
[street, apartment, city_province_postal_code].compact
end
def display_address
address_lines.join("\n")
end
end
We've been shifting the method signature of display_address
from originally accepting an argument, to then not accepting one, to now requiring one. That's generally a bad thing to change since it causes a cascade of changes for any code that uses the particular method. I'd rather not switch back now, so what I can do is provide a way for the template to get the data it needs.
I'm happy to know how to use Forwardable because I can still keep my template code short and sweet. Here's what we can do. First, lets change hte way we interact with the template:
class Address
def display(template)
if protect_privacy?
template.with_address(private_version)
else
template.with_address(self)
end
template.display_address
end
end
Next, we can alter the template by creating the with_address
method:
class Template
def with_address(address)
@address = address
end
end
Then, we can alter the line where we use attr_accessor
to instead query for information from the address and use it as our value object:
require 'forwardable'
class Template
extend Forwardable
delegate [:province, :postal_code, :city, :street, :apartment] => :@address
end
As long as we provide an object which has all of those required features, our Templates will work just fine.
Here's the final result for our Template:
require 'forwardable'
class Template
extend Forwardable
delegate [:province, :postal_code, :city, :street, :apartment] => :@address
def with_address(address)
@address = address
end
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, apartment, city_province_postal_code].compact
end
def display_address
address_lines.join("\n")
end
end
With this change, the Template is still responsibile for only the proper display of data and will handle missing data appropriately. Our Address is responsible for the data itself; it will make decisions about what the data is, and whether or not it should be displayed with a given template.