Often the rules we create are defined by their exceptions.
It is difficult to create a program which continually passes objects and never returns data. Often the first rule of "Always return self" is met with immediate rejection because it's easy to see the difficulty you'd encounter if that rule is continually followed for every object.
In my presentation for RubyConf, I showed how we break the rules to allow value objects to handle data for a template. I previously wrote about the approach I used in the presentation to push data into a value object.
class Address
def display(template)
if protect_privacy?
template.display_address(private_version)
else
template.display_address(public_version)
end
self
end
end
In the sample above, an Address instance commands a template to display_address
with different versions of data: private_version
or public_version
. This makes a flexible interface that allows Address
to create any number of different versions if necessary. Perhaps the requirements will demand a semi_public_version
in the future; our design of the template need not change.
This is a great way to break the rules. Value objects allow us to parameterize a collection of data in a single object. The alternative to this approach would be to use setter methods on the template object:
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
self
end
end
We can plainly see that although the code follows the rules by commanding the template object, there's also quite a lot happening in this display
method on Address
. If the requirements change we might feel encouraged to complicate the unless
block or "refactor" it into a case
statement. While that might solve our problem, the resulting code could lead to some difficult to read and understand implementation details.
By breaking the rules with a value object we can better encapsulate the ideas in a private address object or public or any other type we desire.
But we're not just breaking the rules inside the Address
methods; the template breaks the rules too. Rule 2 says that objects may query themselves
and subsequently means they should not query other objects. But by choosing to break the rules we make a design decision at a specific location to make things better.
No matter what rules you follow, you decide not only to follow them, but decide to break them as well. To make your program easy to understand and to create reasonable expectations, you can lean on creating barriers. Preventing yourself from doing one thing frees you to do another.
Embrace constraints.
How do you add constraints to your programs? What are you better able to do by adding restrictions?