My avid use of Forwardable helps me simplify my code to raise up the important parts. When things still end up too complicated, I can reach for null objects to ease my commands into place.
class Person def initialize(address) @address = address end def address @address || DefaultAddress.new end end
As we've seen in this code, any Person object will always have an address, but what I do with that Person or address is what can cause some problems in the future.
Displaying an address can sometimes be a complicated matter.
Asking for too much
In the case of displaying the address for a particular person, we might want certain formatting depending upon the available details.
If we only have a person's city, or just city and state, we'd probably only show that. An easy way is to just check for those attributes on the address:
class Person def display_address "".tap do |string| string << address.street unless address.street.nil? string << address.city unless address.city.nil? string << address.province unless address.province.nil? string << address.postal_code unless address.postal_code.nil? end end end
This is easy, but introduces a problem if the display of the address ever changes. With the above code, we are asking for a lot of information from the address. It would be more flexible to move this into the address itself.
class Person def display_address address.display end end class Address def display "".tap do |string| string << street unless street.nil? string << city unless city.nil? string << province unless province.nil? string << postal_code unless postal_code.nil? end end end
This simplifies the knowledge we store in the Person class for interacting with an address. Now all we need is to know is that the address has a
As an added benefit, it's far shorter and easier to read.
Being prepared for changes
What happens when we need to output a person's address in both an HTML page and a text-only email?
In one case we'll need a simple line break, and in another we'll need HTML formatting such as a
<br /> element.
Here's an example:
123 Main Street Arlington, VA 22222
An updated version of our code which would skip missing data and create our desired output would look like this:
class Address def display province_and_postal_code = [province, postal_code].compact.join(' ') province_and_postal_code = nil if province_and_postal_code.empty? city_province_postal_code = [city, province_and_postal_code].compact.join(', ') city_province_postal_code = nil if city_province_postal_code.empty? [street, city_province_postal_code].compact.join("\n") end end
With simple text a newline is all we need, but in HTML we'll need to provide a break:
123 Main Street
Arlington, VA 22222
We could add features to our Address class to handle changes like this:
class Address def display(line_break) # same code as above here ... [street, city_province_postal_code].compact.join(line_break) end end
That seems like a simple change, but in order to use it the Person would need to know what type of line break to provide to the address. This would push the decision for how to display the address far away from the address itself.
Instead, we could keep the information for formatting inside an object whose responsibility is specifically for formatting the information. If we require a new type of format, we would then only need to create an object for that format.
For example, our standard template could handle the data with a newline character.
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
Then our HTML template could handle the data with proper line breaks for it's format:
class HtmlTemplate def display_address(address) # same code as above here ... [address.street, city_province_postal_code].compact.join("
") end end
We should move the duplicated code to a common location so that each template could share it, but this will do for now.
So how might our Person and Address objects use these templates?
All we'll need to do is change our
display methods to accept a template for the formatting.
class Person def display_address(template) address.display(template) end end class Address def display(template) template.display_address(self) end end
If we add new formats, neither or Person nor our Address class will need to change. There's still more we can do to guard against changes but I'll write more about that next time.