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:
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.
How do you add constraints to your programs? What are you better able to do by adding restrictions?