Flexible Design

Software does not exist in a vacuum. It interacts with environment and the environment interacts with it. The environment is market forces, users, external systems, operating systems, competing software, changes in law etc. It evolves, either it improves or decays over time. The only thing that is constant is change demanded by the environment.

The Law of Change: The longer your program exists, the more probable it is that any piece of it will have to change. Max Kanat-Alexander in Code Simplicity book

You need to work on a existing code base in order to :

  1. Improve Performance
  2. Improve the Design
  3. Fix Bugs
  4. Enhance existing features
  5. Upgrade to newer software it depends on
  6. Add features
  7. Remove features

How do we evaluate a design that will make the software easy to do all of the above? If we order from the most to least desirable ways to achieve quality, they are:

  1. Data driven or meta-programming
  2. Additive change
  3. Localized modification.

Just because meta-programming is on the top of the list, I am not advocating that is the first choice for every design problem. As long as meta-programming is used to achieve a good design that obeys good design principles, it is ok. Remember the two golden rules from Code Simplicity book:

  • It is more important to reduce the effort of maintenance than it is to reduce the effort of implementation.
  • The effort of maintenance is proportional to the complexity of the system.

Localized Modification

According to the dictionary, localized means restrict something to a particular area. We change only one specific location in our existing code base to implement a new feature. The changed code must be deployed. Before you can run the example code, you need to install highline and clipboard gems.

$gem install highline
$gem install clipboard

Here is the localized modification version of the password recall script:

#!/usr/local/bin/ruby
require 'digest/sha1'
require 'highline/import'
require 'clipboard'


def unlock_password(account, domain)
  salt = ask("Enter your secret key : ") do |q|
    q.echo = false
    q.verify_match = true
    q.gather = {"Enter your secret key" => '', "" => ''}
  end

  password = Digest::SHA1.hexdigest(domain + account + salt)
  Clipboard.copy(password)  
end

choose do |menu|
  domain = ask("Enter the website : ")
  menu.prompt = "Please make a selection : "

  menu.choice :yahoo do 
    unlock_password('email', domain)
    say("Yahoo password copied.") 
  end

  menu.choice :google do 
    unlock_password('email', domain)
    say("Gmail password copied.") 
  end

  menu.choice :microsoft do 
    unlock_password('email', domain)
    say("Live password copied.") 
  end

end

Here we modify just one file and we add a new site to menu.choice call.

Additive

We add new code to the existing system without modifying the existing code to implement a new feature. Risk of introducing bugs to existing code is very low. New code must be deployed. This will use polymorphism so that the new object introduced will have the same interface that the existing code uses.

Here is an example from Rails Antipatterns by Chad Pytel and Tammer Saleh book that I have improved the design by moving it from localized modification to additive change.

Before (Solution in the Book)

class OrderConverter
  def initialize(order)
    @order = order
  end

  def to_xml
  end

  def to_json
  end

  def to_csv
  end

  def to_pdf
  end
end

oc = OrderConverter.new(order)
oc.to_xml

This solution needs localized changes to add a new conversion format for the order.

After (My Improved Solution)

Here is my solution that allows additive changes:

class Order
  attr_reader :amount, :number

  def initialize(amount, number)
    @amount = amount
    @number = number
  end
end

class OrderXmlConverter
  def initialize(order)
    @order = order
  end
  def convert
    "<order><amount>#{@order.amount}</amount><number>#{@order.number}</number></order>"
  end
end

Instead of hard-coding class name, you can use const_get to dynamically instantiate a class:

order = Order.new(19, 2)
format = 'Xml'
class_name = Object.const_get("Order#{format}Converter")
converter = class_name.new(order)
puts converter.convert

In rails, you can use constantize method:

class_name = "Order#{format}Converter".constantize

By following a convention in naming the converter class, we eliminate dependency on a specific class name. In order to add a new format, for instance json, we add a new class OrderJsonConverter which has the same interface convert that returns JSON representation. Uniform interface allows additive change. We end up with small classes that is focused on doing one thing really well, they all have the same interface, convert in our example.

Data Driven

New data is added to make the system implement a new feature. This is the most flexible design. Probably this design will demand the highest effort of implementation. No code deployment necessary. The example below requires reading the value of the array items from an external configuration file.

#!/usr/local/bin/ruby

require 'digest/sha1'
require 'highline/import'
require 'clipboard'

def unlock_password(account, domain)
  salt = ask("Enter your secret key : ") do |q|
    q.echo = false
    q.verify_match = true
    q.gather = {"Enter your secret key" => '', "" => ''}
  end

  password = Digest::SHA1.hexdigest(domain + account.to_s + salt)
  Clipboard.copy(password)  
end

choose do |menu|
  domain = ask("Enter the website : ")
  menu.prompt = "Please make a selection : "

  # This is hard-coded. You must read the values of the list
  # from an external configuration file to make it
  # data driven that does not require source code changes
  # to add a new site.
  items = [:yahoo, :google, :microsoft]

  items.each do |item|
    menu.choice item do 
      unlock_password(item, domain)
      say("#{item.to_s} password copied to clipboard.") 
    end
  end

end

Design Techniques

What are the design techniques to achieve these three kinds of design?

  1. Localized changes are better than changes that ripple across your code base.
  2. Additive changes use polymorphism, meta-programming etc. It obeys Open Closed Principle if properly designed. New code is added with no modification to existing code.
  3. Data driven technique obeys Open Closed Principle. No change is made in existing code. No new code is added.

Summary

In this chapter we saw three different kinds of design that gives different levels of flexibility. Sometimes you have to make a trade off between complexity and flexibility. You can recognize these different types of flexibility in your code and make decisions based on your current requirements.

Reference

Rails Antipatterns by Chad Pytel and Tammer Saleh