Red Green Repeat Adventures of a Spec Driven Junkie

Interface & Driver Theory and Application in Ruby

I wanted to understand the “interface” and “driver” design pattern better, so I spent time exploring it and coding it out.

Things I want to learn:

  • how to write an interface that can work with different drivers
  • write different drivers that can work with the interface
  • use dependency injection to work with different systems using single interface

Requirements

  • Ruby 2.0.0 or greater installed (I developed the code on Ruby 2.5.1)
  • If you want to play with the code, it is here.

Motivation

Imagine being able to output a command line program to screen AND to file? Normally, every language has direct hooks to access the screen and the file system.

If you write the program to work with the screen, changing it to work with a file isn’t that hard.

But, what if the program needs to interact with BOTH screen and file at the same time? The screen is for a person using the program, the file is for another piece of software to use the program (say a machine learning or artificial intelligence system.)

What if there’s need for another requirement for the program to interact with another system, say: a intergalatic satelite system?? Touch every piece of the system again?? Add more logic everywhere??

Solution: create a generic interface and have drivers to connect to the screen, or file system, or intergalatic satelite system.

Definitions

  • Interface: It’s basically a higher level abstraction of the drivers. The interface depends on the driver to do its work.
  • Driver: It’s the lower level abstraction from the interface. The driver does not depend on the interface to do its work.

General Interface

I will start from theory and start with a general outline of what an interface is like. I will start with a generic Interface class that implements basic functions, which could look like this in code:

class Interface
  @driver

  def initialize(driver)
    @driver = driver
  end

  def write(items)
    @driver.write(items)
  end

  def read(criteria)
    @driver.read(criteria)
  end

  def close
    @driver.close
  end

  def clear
    @driver.clear
  end
end

As a demonstration, we are going with a lightweight driver, so the interface functions mirrors the driver functions.

The interface dictates what functions the system can use. In this case, write, read, close, and clear. Other functions using this interface have to make use of these functions.

The driver must implement a version of these functions from the interface to be useful as a driver. If the driver does not support one of these interface functions, then it would not be a compatible driver for the interface.

General Driver

Following the General Interface, a driver that conforms to that interface must support the basic functions.

class Driver

  def write(item)
    driver.write(item)
  end

  def read(criteria)
    driver.database.read(criteria)
  end

  def close
    driver.close
  end

  def clear
    driver.clear
  end
end

The driver is specific to the actual subsystem that it works with from the interface. If it is a driver that works with system display, the driver could be: STDIN or STDOUT.

Putting Them together

So, we have a general interface and driver, how do we get them to work together?

With Dependency Injection!

Yes, it sounds really cool, but all dependency injection really means is:

Pass in all the dependencies of a function in externally!

Interface and drivers are the perfect example of depenency injection. The interface can’t work without a driver, so pass in the driver for the interface!

Given the Interface and Driver classes from above, this is how to use them:

new_driver = Driver.new
new_interface = Interface.new(new_driver)

new_interface.write("hello world")
new_interface.read("give me some input")
new_interface.clear
new_interface.close

This is all there is to interface and driver with dependency injection. Easy, right?

Let’s Get It In Code!

So I basically fell into the trap of almost every explanation I have read about interface and driver with depenency injection: it’s too abstract. I want to see working code behind it.

Luckily, I have a quick implementation in Ruby that has no depenencies (get it??)

I have created two drivers, a FileDriver that interacts with files, and a ScreenDriver, that interacts with standard inputs, screen and command-line. The UserInterface uses both of them to achieve the same results:

class FileDriver
  @file

  def initialize(file_name)
    @file = File.open(file_name, "wb")
  end

  def write(text)
    @file.write(text)
  end

  def read(file_name = "user_input.txt")
    raise MissingFileError unless File.open(file_name)
    screen = ScreenDriver.new
    screen.write("reading from #{file_name} file")
    screen.write(File.read(file_name))
  end

  def close
    @file.close
  end

end

class ScreenDriver
  def iniitialize
  end

  def write(text)
    puts text
  end

  def read
    puts "type in some input for the ScreenDriver"
    puts "the input received is: " + STDIN.gets
  end

  def close
  end
end

class UserInterface
  @interface

  def initialize(options)
    @interface = case options
                 when :screen
                   ScreenDriver.new
                 when :file
                   FileDriver.new("file_driver_test.txt")
                 else
                   raise ArgumentError
                   exit 0
                 end
  end

  def write(text)
    @interface.write(text)
  end

  def read
    @interface.write("getting input")
    @interface.read
  end

  def close
    @interface.close
  end
end

screen_interface = UserInterface.new(:screen)
screen_interface.write("screen test")
screen_interface.read
screen_interface.close

file_interface = UserInterface.new(:file)
file_interface.write("file test")
file_interface.read
file_interface.close

The key part is the similarity the actions on the interface, even though working with a file and the screen utilize different functions of Ruby.

Download the repository here and play with it. Reading about it is nice, working with it is eye-opening.

Extending

What’s even cooler? Let’s say we need to have this program interact with HTML as well, how could that happen with this setup?

By adding an HTMLDriver!

class HTMLDriver
  def initialize(html_filename = "interface.html")
    @file = File.open(html_filename, "wb")
    @file.write("<html>\n<body>\n")
  end

  def write(text)
    @file.write("<p>\n")
    @file.write(text)
    @file.write("</p>\n")
  end

  def read
    raise FunctionError
  end

  def close
    @file.write("\n</body>\n</html>")
    @file.close
  end
end

Now any part of the program that wants to work with HTML can just create an interface that works with the HTML driver by:

html_interface = UserInterface.new(:html)
html_interface.write("writing to html file now")
html_interface.close

Conclusion

It’s a bit of a whirlwind intro into interface and driver abstraction, but once I saw it in code, things really clicked, even opening my imagination to other possibilities, which I will probably explore in the next weeks.

Key ideas:

  • have an interface class that abstracts common driver elements together.
  • have a driver that implements all the basic functions used by the interface.
  • when using the interface, use dependency injection. Create the interface by including the driver you want the interface to use.

That’s it for me this time. I’ll share more as I explore this topic!