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:
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.
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:
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:
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
!
Now any part of the program that wants to work with HTML can just create an interface that works with the HTML driver by:
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!