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 :
- Improve Performance
- Improve the Design
- Fix Bugs
- Enhance existing features
- Upgrade to newer software it depends on
- Add features
- 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:
- Data driven or meta-programming
- Additive change
- 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?
- Localized changes are better than changes that ripple across your code base.
- 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.
- 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