Enforcing encapsulation with East-oriented Code

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!

Get a FREE sample chapter

Get a sample chapter of Clean Ruby and you'll be added to my periodic newsletter of helpful Ruby tips.