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
endAs 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
endThe 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.closeThis 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.closeThe 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
endNow 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.closeConclusion
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!