Dependency Inversion Principle
In this chapter, we will explore Dependency Inversion Principle by examples.
Steps
Step 1
Let's write a program to :
1. Read from keyboard.
2. Write to a terminal.
Here is the program:
def copy
message = gets
puts message
end
copy
We can run this:
$ ruby copy.rb
Hello
Hello
Step 2
We now want to accommodate changes to our program that can:
1. Read from a tape reader
2. Write to a terminal
Here is the modified program:
def read_tape
p 'Input from tape reader'
end
def copy(tape_reader = false)
if tape_reader
message = read_tape
else
message = gets
end
puts message
end
copy(true)
We can run it:
$ ruby copy.rb
Input from tape reader
Input from tape reader
Step 3
We now need to accommodate changes to our program that can:
1. Read from tape reader as before
2. Write to paper tape punch
Here is the modified program:
def read_tape
p 'Input from tape reader'
end
def write_to_paper_tape_punch(output)
p 'Output to paper tape punch'
end
def copy(tape_reader = false, paper_tape_punch = false)
if tape_reader
message = read_tape
else
message = gets
end
if paper_tape_punch
write_to_paper_tape_punch(message)
else
puts message
end
end
copy(true, true)
The solution is becoming messy with lot of conditionals. When we add more types of input and output coding down this path will lead to very difficult to maintain codebase.
Applying Good Design Principles
We will apply the following three basic principles:
- Separate things that change from things that stays the same. Encapsulate what varies behind a well-defined interface.
- Program to interfaces, not implementations. This exploits polymorphism.
- Depend on abstractions. Do not depend on concrete classes.
The input device and output device can change. We can encapsulate them behind the interfaces named read and write. The code must use the newly defined read and write interfaces. This results in the code depending on abstractions.
class Copier
def initialize(reader, writer)
@reader, @writer = reader, writer
end
def copy
message = @reader.read
@writer.write(message)
end
end
class KeyboardReader
def read
gets
end
end
class PrinterWriter
def write(output)
p "Writing #{output} to printer"
end
end
Copier class now depends on stable interface read and write. The implementation of KeyboardReader and PrinterWriter is now hidden behind the well defined read and write interface. We can use this new design to copy from any input source to any output source without modifying the Copier class.
reader = KeyboardReader.new
writer = PrinterWriter.new
copier = Copier.new(reader, writer)
copier.copy
We can run this program.
$ ruby copy.rb
This is a test
"Writing This is a test\n to printer"
Dependency Inversion Principle
It states:
- Details should depend on abstractions.
- Abstractions should not depend on details.
- High level modules should not depend upon low level modules. Both should depend upon abstractions.
Compare the dependency of:
Copy
Read Keyboard WriterPrinter
vs
Copy
Reader : KeyboardReader
Writer : PrinterWriter
The interface in KeyboardReader and PrinterWriter is implicit in Ruby. We conform to the read and write interface instead of explicitly saying we implement those interfaces in the code. The copier class is reusable with different input and output devices.
Code Breaker Game Example
Let's look at the code example for Codebreaker game from The Rspec Book. The solution in the book does not apply the DIP.
Before
module Codebreaker
class Game
def initialize(output)
@output = output
end
def start(secret)
@secret = secret
@output.puts 'Welcome to Codebreaker!'
@output.puts 'Enter guess:'
end
end
end
g = Codebreaker::Game.new($stdout)
g.start('sekret')
This is a good example that shows using BDD or TDD does not make the design emerge magically. You must apply good design principles consciously and deliberately. Let's look at the solution after applying the DIP.
After
module Codebreaker
class Game
def initialize(writer)
@writer = writer
end
def start(secret)
@secret = secret
@writer.write 'Welcome to Codebreaker!'
@writer.write 'Enter guess:'
end
end
end
class StandardConsole
def write(message)
$stdout.puts(message)
end
end
writer = StandardConsole.new
g = Codebreaker::Game.new(writer)
g.start('sekret')
It's surprising to find coding solution from a published book that has gone through technical review come up short in design.
Summary
In this chapter, we applied the three basic principles of good design:
1. Separate things that is likely to vary
2. Hide them behind a well defined interface
3. Write your program to use the stable interfaces instead of depending on concrete details.
to come up with a flexible design that is easy to maintain and extend.