Often our programs become complicated inadventently. We don't intend to put things it the wrong place, it just seems to happen.
Most of the time it happens to me when I allow my objects to leak information and eventually their responsibilities.
In recent articles I showed code that handled displaying address details and how to separate the responsibility for formatting from the responsibility for data. But there's still a problem which would allow me or someone else to unintentionally leak responsibility from these objects.
It's a good idea to be guarded against how much you reveal from an object; you never know how someone might use it in the future.
In our Template code, we provide a number of values about the object: province, postal_code, city, street, apartment, province_and_postal_code, city_province_postal_code, address_lines, and display_address. With each attribute provided, we introduce the ability to other objects to query the information and make decsions based upon the answer.
It's far too easy to write these types of queries:
if template.province == "..."
if template.city == "..." && template.postal_code == "..."
if template.city_province_postal_code.include?("...")
But what would our code look like if we couldn't do this? What if there were no questions to ask?
What if the only accessible information from our template was the display_address
used to show the formatted data?
require 'forwardable'
class Template
extend Forwardable
def display_address
address_lines.join("\n")
end
def with_address(address)
@address = address
end
private
delegate [:province, :postal_code, :city, :street, :apartment] => :@address
def province_and_postal_code
# ...
end
def city_province_postal_code
# ...
end
def address_lines
[street, apartment, city_province_postal_code].compact
end
end
By moving most of our methods under the private
keyword, our Template interface has shrunk significantly. Now all we'll have to handle and all other objects will need to know about is the display_address
and with_address
methods.
East vs. West
The changes we made make a significant restriction on the questions that we can ask about an object. This is where the idea of East-orientation comes in.
If we imagine a compass applied to our source code we'd see that any query, any if
, will send the information flowing westward.
# <---- information travels West
if template.city == "..."
The if
handles the execution of the algorithm. But by removing methods from the public interface which provide attributes like above, we better encapsulate the data in the target object. Our template here could not answer a question about its city attribute.
Instead, the code which uses the template would be forced to command the template to perform a particular action. The body of the if
could instead become a method on the template object.
# ----> information travels East
template.perform_action
The template can make it's own decisions about what to do when told to perform some action.
Enforce encapsulation with return values
An easy way to ensure that our code encourages commands, discourages queries, and enforces encapsulation is to control the return values of our methods.
The best thing to return is not necessarily the result of the method, but the object performing the method. It's as simple as adding self
to the end of the method block.
Here's what that might look like:
class Template
def with_address(address)
@address = address
self
end
end
By adding self
there, each time we set the address value object using with_address
we are given the object itself back, instead of the value that we passed to it.
# Without appending "self"
template.with_address(address) #=> address
# After appending "self"
template.with_address(address) #=> template
This becomes a powerful change to the way we interact with the template object. It enforces the encapsulation of data of the template and it forces us to think more about sending messages to our objects and allowing them to implement the solution.
When we return the object itself, we can only continue operation on that object.
The added benefit is that our code will become more concise. We will prevent unintentional dependencies between objects. And we can chain our commands together; it's all the same object:
template.with_address(address).display_address
See the flow at a glance
By using a visual compass to guide us through our code, it's easy to step back and see exactly where we leave our objects leaking information and responsibility.
Each time we query an object, each time we set a variable, we should now see the westward flow of information.
By simply returning self
from our methods, we will force the hand of every developer to think with East-orientation in mind. By only working with the same object we will get back to using objects in the way that tends to be the most useful: for handling messages and implementing the required algorithm.
One last issue is that of the display_address
method. Currently it returns the string representation of the address and not the template itself.
We can change that. What you do depends on how you're using a template. Perhaps our base Template will output details to STDOUT, or perhaps to a text file. Here's how we'd take care of that:
class Template
def display_address
STDOUT.puts address_lines.join("\n")
# or perhaps File.write ...
self
end
end
Try this with your code. Return "self" and see how it changes your thinking. Scan for westward flow of information and see how you can push responsibilities into the appropriate objects by heading East.
From now until the end of the year, you can get Clean Ruby for only $42 (the original pre-release price) by using this link. It'll only be valid this year (2014) and will go up automatically in January 2015. Merry Christmas!