The Device Driver Abstraction
How a simple idea changed software design forever
It was the dawn of computers and software. Operating systems were being developed, and there were still no agreed-upon abstractions. Then came a simple abstraction: the device driver. The device driver enabled the development of software that was not locked to a specific hardware; it allowed developers to write code that worked with any hardware. This seemingly simple change made software development a lot simpler. It is something we take for granted today, but that we forget when we develop software. Why do we keep on forgetting this simple idea? In this article, I’ll explain what the driver abstraction is, how it came to be, and the problems it tried to solve. I will also show why we keep forgetting about it, and the costs we pay every day for not learning from our past.
Hard Drive
History
When computers first appeared, they simply ran one program at a time, with no operating system at all. This meant that every program needed to know the internals of the hardware, of how to control each and every device.
Imagine that for a moment. Imagine that you wanted to read a file from persistent storage. If the computer had a tape drive, you would have to program how to control that tape drive; if it had a floppy drive, who had to control it? If the computer had a gigantic, heavy and expensive hard drive that required a whole room, you would have to control it. Maybe I am pushing the limit of the analogy here to prove my point: your program, your code, had to control everything.
Then came the operating system. The operating system introduced an abstraction layer that enabled the application developers to focus on the applications and the operating system developers to focus on the operating system. With that abstraction came the concept of a device driver. The idea behind the device driver was to have a program that was responsible for controlling a specific piece of hardware, liberating the application and operating system developers from knowing the internal details of the hardware. In my view, this is the dawn of Dependency Inversion Principle.
The Dependency Inversion Principle enabled applications to dictate what they would need from the operating system. For instance, we all take for granted all the functions to access a file system, but how that file system is implemented, we don’t care as much.
It was also the Dependency Inversion Principle that enabled operating systems to dictate what they needed from persistent storage devices. Think of all the block devices that the Linux Kernel supports: you can have virtually any file system implementation using any block device, and your applications won’t see any difference.
This inversion of dependencies is what made all the advancements possible, made our applications simpler, and enabled us to focus on our applications, while file system developers focused on their file system implementation and hardware manufacturers on their device driver implementations. We do not need to rebuild and redistribute our applications whenever a new storage device is available.
How we almost forgot history when we had printers
If you have been using computers as long as I have, you might remember the time when you had to use the correct word processor, the one that supported your printer, because not all word processors supported all printers. This fight was strong for a long time, but I believe we are culminating with the Internet Printing Protocol and Postscript. Most printers today have an open network port, accepting connections where your computer can send the postscript document to print. Before that, you would have to hunt down drivers, and even them, some software simply refused to work properly. It is as if we forgot how to do proper abstracts, that we forgot the lessons from the past about inverting dependencies and building device drivers.
Why do we keep forgetting about history when we write new applications?
Now that we have remembered how crucial device drivers have been for our sanity, it is time for us to think about when was the last time we saw this abstraction, this inversion of dependencies in our code.
How often do you see code that relies heavily on the database connection API? For instance, in Java, how many times have you seen code that relies on JDBC or JPA? Why do we keep writing code that depends on the device driver?
Don’t get me wrong. JDBC and JPA are good abstractions, but they are too low-level for our applications. What we want is an abstraction that makes our code unaware if the storage is a relational database, if JPA or JDBC is used. We want to define a dependency as having some kind of transactional storage; we don’t care about the underlying technology. It can be a no-sql Database, a file system, or a version control system: we simply do not care. We want the core of our application to be agnostic of all that.
The reasons behind this are twofold: we want our system to be easily adoptable, and we want our system to be easily tested. It is a lot easier to test our system if we have a device driver for in-memory storage. Not only does it make testing easier, it also makes our tests faster. Why don’t we want to capitalize on that?
Dependency Inversion And Test Driven Development
One of the reasons I like test driven development, is that it forces me to think of how to test the system. It forces me to define boundaries for my modules. Those boundaries are prime spots to use Dependency Inversion. In my tests, I can assume I have some storage gateway that defines the API for my storage. To write tests and make them fast, I use an in-memory storage gateway. In production, I implement the gateway for whatever storage mechanism we want to use.
Yes, this is where the Dependency Inversion Principle and Test Driven Development touch the architecture of our system: they allow us to delay the decisions. It is by enabling us to delay these decisions that we can build better architectures.
Dependency Inversion And Architecture Decisions
One of the hardest lessons to learn when developing any software system is to delay decisions as much as possible. We never star by saying: let’s use a relational database, or, let’s use an object relational mapper. What we always want is to delay these decisions until we can make an informed decision, until we know more about the system.
Think about it. Do you know if your system will run on a mobile phone? Do you know if it will run in the cloud and have to support millions of users? Will it have to support versioning? All these decisions have an impact on the technology choices you make, but until we know about them, we need to delay our decisions.
You see, unless we have a time machine, we never know what new requirements our system will have. If we tie our system to a decision that is hard to change, we can get in trouble.
Imagine you are making a Wiki. Probably one of the things that comes to mind is having a relational database to store the pages. Why not? We would have transactions and other things. But then someone wants to add versioning to the wiki. How do you do that? If you delayed the decision and stuck to a simple file system, you could simply introduce a version control system like git and have versioning almost for free.
Make your life simpler
This is the kind of decision-delaying strategy that makes our lives as software developers easier. Why aren’t we leveraging them?
I believe that we have many developers who have never fully understood this technique. There have been some movements against the SOLID principles, making them look bad when they are not properly understood. We have come to believe that object oriented programming is about creating large class hierarchies, when it is quite the opposite. Our goal is to invert dependencies and have the system loosely coupled, composed of modules/objects that pass messages.
I also believe that sometimes this technique is not known, but we live in the information age. We should learn from the past and leverage their knowledge.
It is also true that in other times, we give in to the pressure to be fast, and we shoot ourselves in the foot, cut off corners and end up with a system that is hard to change.
The next time you develop code, just use your head: invert those dependencies and use the device driver technique. Not only will you make more flexible systems, but you will also make systems that are easily tested and that let you sleep at night.