Preferring value objects or setters and arguments

The problem with programming can be that there are so many ways to solve a problem. For each solution there are arguments for it and arguments against it.

In recent articles I've written about moving responsibilities into a template object and out of the objects which use them for display.

When the template code first began, its use was extremely simple:

class Address
      def display(template)
        template.display_address(self)
      end
    end

By making changes to the template to allow for shared behavior among different types of templates, the way in which our Address class used it became a bit more complex:

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
      end
    end

Originally the Address class knew of two features of the template, that it had a display_address method, and that the method took a single argument intended to be the address.

After some rework, the template became easier to manage and it became easier to make alternative formats, but the changes burdened the user of the object with the need for more knowledge. The Address objects now also need to know that there are setters for street=, apartment=, postal_code=, city=, and province=. It also needs to be implicitly aware that the template could render incomplete data; we know we aren't required to set nil values for certain attributes.

Getting back to simple

We made good changes for the template, but I want that simple interface back. I want my address to act as a value object instead of needing to keep track of passing so many arguments.

While I want to go back to this:

class Address
      def display(template)
        template.display_address(self)
      end
    end

I need a way to handle the case where we have sensitive data. What about that protect_privacy? method?

Here's what we could do:

class Address
      def display(template)
        if protect_privacy?
          template.display_address(private_version)
        else
          template.display_address(self)
        end
      end

      def private_version
        self.class.new_with_attributes(city: city, province: province)
      end
    end

With this change, the Address can still make a decision about displaying private data and it merely sends that version along to the template. I'm leaving the implementation of new_with_attributes up to imagination, but we'll assume it will set the attributes we've provided on a new instance and return that.

Our template, when last we saw it, looked like this:

class Template
      attr_accessor :province, :postal_code, :city, :street, :apartment

      def province_and_postal_code
        # ... return the combined value or nil
      end

      def city_province_postal_code
        # ... return the combined value or nil
      end

      def address_lines
        [street, apartment, city_province_postal_code].compact
      end

      def display_address
        address_lines.join("\n")
      end
    end

We've been shifting the method signature of display_address from originally accepting an argument, to then not accepting one, to now requiring one. That's generally a bad thing to change since it causes a cascade of changes for any code that uses the particular method. I'd rather not switch back now, so what I can do is provide a way for the template to get the data it needs.

I'm happy to know how to use Forwardable because I can still keep my template code short and sweet. Here's what we can do. First, lets change hte way we interact with the template:

class Address
      def display(template)
        if protect_privacy?
          template.with_address(private_version)
        else
          template.with_address(self)
        end
        template.display_address
      end
    end

Next, we can alter the template by creating the with_address method:

class Template
      def with_address(address)
        @address = address
      end
    end

Then, we can alter the line where we use attr_accessor to instead query for information from the address and use it as our value object:

require 'forwardable'
    class Template
      extend Forwardable
      delegate [:province, :postal_code, :city, :street, :apartment] => :@address  
    end

As long as we provide an object which has all of those required features, our Templates will work just fine.

Here's the final result for our Template:

require 'forwardable'
    class Template
      extend Forwardable
      delegate [:province, :postal_code, :city, :street, :apartment] => :@address

      def with_address(address)
        @address = address
      end

      def province_and_postal_code
        value = [province, postal_code].compact.join(' ')
        if value.empty?
          nil
        else
          value
        end
      end

      def city_province_postal_code
        value = [city, province_and_postal_code].compact.join(', ')
        if value.empty?
          nil
        else
          value
        end
      end

      def address_lines
        [street, apartment, city_province_postal_code].compact
      end

      def display_address
        address_lines.join("\n")
      end
    end

With this change, the Template is still responsibile for only the proper display of data and will handle missing data appropriately. Our Address is responsible for the data itself; it will make decisions about what the data is, and whether or not it should be displayed with a given template.

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.