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.