Localized Change vs Additive Change
To learn how to eliminate conditionals and come up with a good object oriented design.
Transformer
Let's write a program to transform a json string or a binary format string.
Steps
Step 1
The transformer that uses conditional:
require 'json'
class Transformer
def initialize(string)
@string = string
end
def transformed_string(type)
if type == :json
JSON.parse(@string)
elsif type == :binary
@string.unpack('B*').first
end
end
end
t = Transformer.new('Hello')
x = t.transformed_string(:binary)
p x
This prints:
0100100001100101011011000110110001101111
Step 2
Let's eliminate the conditional in transformed_string method:
require 'json'
class Transformer
def initialize(string)
@string = string
end
def transformed_string(transformation)
transformation.transform(@string)
end
end
class JSONTransformation
def self.transform(string)
JSON.parse(string)
end
end
y = Transformer.new('{"foo": "bar"}').transformed_string(JSONTransformation)
p x
This prints : { "foo" => "bar" }
Step 3
Let's now implement the binary transformation:
require 'json'
class Transformer
same as before
end
class BinaryTransformation
def self.transform(string)
string.unpack('B*').first
end
end
x = Transformer.new('Hello').transformed_string(BinaryTransformation)
p x
This prints:
0100100001100101011011000110110001101111
Step 4
Let's change the setter dependency injection to constructor dependency injection and cleanup the code a bit:
require 'json'
class Transformer
def initialize(type)
@type = type
end
def transform(string)
@type.transform(string)
end
end
class BinaryTransformation
def transform(string)
string.unpack('B*').first
end
end
transformer = Transformer.new(BinaryTransformation.new)
x = transformer.transform('Hello')
p x
This prints:
0100100001100101011011000110110001101111
Step 5
We can implement the JSON tranformation:
class JSONTransformation
def transform(string)
JSON.parse(string)
end
end
t = Transformer.new(JSONTransformation.new)
y = t.transform('{"foo": "bar"}')
p y
Step 6
Let's implement MD5 for fun:
require 'digest'
class MD5Transformation
def transform(string)
Digest::MD5.hexdigest string
end
end
s = Transformer.new(MD5Transformation.new)
z = s.transform('Hello')
p z
we eliminated conditionals and replaced it with well-defined abstraction. We went from isolated changes to additive changes to implement new functionality. In this case trasform method. This is the main theme of Design Patterns: Elements of Reusable Object-Oriented Software book.
Step 7
We can do even better by replacing these anemic classes with blocks.
require 'json'
class Transformer
def transform(string)
yield(string)
end
end
binary_transformer = ->(x) { x.unpack('B*').first }
transformer = Transformer.new
a = transformer.transform('Hello', &binary_transformer)
p a
json_transformer = ->(y) { JSON.parse(y) }
b = transformer.transform('{"foo": "bar"}', &json_transformer)
p b
require 'digest'
md5transformer = ->(z) { Digest::MD5.hexdigest(z) }
c = transformer.transform('Hello', &md5transformer)
p c
Summary
In this chapter, we used dependency injection to vary the implementation for transforming a given string. We improved the design by going from Localized Change to Additive Change. The examples used in this chapter is based on SOLID Review: Dependency Inversion Principle.