I start each day with a cup of coffee.
The process is the same — I choose a mug, fill my Keurig machine with water, place a k-cup inside, and I hit the start button. I hit the start button having complete trust that I’ll have a steaming cup of joe in a few minutes, because that’s just what a Keurig machine does.
The thing is, I have no idea how my Keurig machine makes my cup of coffee. I have my speculations, of course, from working previously in a coffee shop with much larger brewers. I can speculate that somewhere along the process, the Keurig machine heats the water I feed it, flowing that water through the k-cup to drain into my mug. However, when I hit that start button, I still do not truly know with complete understanding how my Keurig machine works. I do not know its implementation; I know what it does, not how it does it. In other words, the Keurig machine has defined an interface that allows me to obtain a cup of coffee, without knowing how it’s being done — satisfying my caffeine addiction.
From Units to Systems to Interfaces
Before going any further, let’s define a few terms:
The word system itself is rather vague and can point towards a multitude of items varying in both essence and complication. A system could refer to something as complex as the nervous system, acting as a network of neurons and cells that carry messages to and from the brain, as well as something as simple as a reusable water bottle, allowing users to consume liquids at ease. The combining theme between these system entities is that they are both made up of units working together for a single purpose. Within a nervous system, the single purpose is to transmit signals to and from different parts of the body. From the central nervous system to the temporal lobe, each unit has a separate, defined responsibility and works together with other units in order to achieve the key purpose of a nervous system, all the while cranial and spinal nerves serve as an interface for the central nervous system from the peripheral nervous system. Similarly, a twisting cap and a grooved edge of a reusable water bottle act as units forming a unified whole to serve as a system with the key objective of allowing a person to drink from it.
Within software, the idea is the same. Applications, no matter the native platform, are built upon multiple units functioning cohesively as a system. These units themselves are made from inner units which the outside world does not know about, nesting down to the lowest levels. Units also make assumptions about other units using the communicating entity in between — namely, an interface. When I say interface, I do not merely mean the interface keyword so often used in languages like TypeScript or Java. No, I mean something more general — I am referring to the point in which systems (possibly unrelated) are given the ability to interact with each other, often times denoted as a public or an external interface. This interface, or contract, fully defines interactions across the boundaries of units that comprise a system. These interfaces should limit as much private implementation knowledge that a unit maintains independently in order to provide the simplest and clearest level of functionality to the consumer of its resource. The consuming unit should not know how something is done, but it should know what it needs achieved and where it needs to go to. In the words of Sandi Metz, this shifts the question at hand from “I know what I need and I know how you do it“ to “I know what I need and I trust you to do your part“. By creating well-defined public interfaces between units, code becomes both easier to read and reason with. Consider the following examples of systems and their respective units and interfaces:
Coffee to Code
Having interfaces that are defined well are of utmost importance, whether for hardware or software. The value remains consistent. In terms of hardware, it would not be a very good user experience if there were individual buttons for each action that went towards the main goal of producing a cup of coffee. What if the user forgot to press one of the buttons? What if a button was pressed in the incorrect order? Why must the workload towards the user be so heavy when the mechanical responsibilities should be delegated to the Keurig machine itself? It is much more logical to have one, clear interface available to the user while still fulfilling the key objective of the system. Scott Meyers has concluded that the most important general interface design guideline is as follows:
“Make interfaces easy to use correctly, and hard to use incorrectly.“ — Scott Meyers
Having well-defined interfaces solves this issue directly and suggests that there is an abstraction of both complexity and functionality. Being able to abstract away details of how something works simplifies the use of the system at hand tremendously, making the job of the user much simpler and straight to the point. This is something that all high-quality interfaces have in common and must adopt to be considered as such. By having well-defined interfaces and units containing distinct and clear responsibilities, it makes it easier for code to be correct in terms of both system goals and remaining at the highest possible level regarding the ability to change or add code to the system in the future. This creates flexible and powerful interactions between units — resulting in robust and highly functional systems.
Let’s see how a system comprised of units with distinct responsibilities results in a clearer interfaces between units and results in an interface from the system that is easer to use for the user.
Consider the code below (written with pseudocode, of course).
Here, we have two systems at hand: BadKeurig and GoodKeurig.
BadKeurig, as the name suggests, is a poorly designed class (or system) that provides a public interface that forces the consumer of the class to know and understand the implementation of how a Keurig machine works. This requires a deeper knowledge of the order in which the functions (or units) must be called, and results in a large, complicated public interface. To get a cup of Joe, the user must call lockLid(), and then sequentially heatWater(), and the list goes on. Though these units may have distinct, responsibilities, the interface provided to the user is complex and could be improved greatly.
However, hope remains. In GoodKeurig, the public interface is designed clearly and simplifies the process for the consumer of the class tremendously. In order to produce a cup of coffee, all the user must know is that there exists a start() function in the GoodKeurig class. The units, or functions of the GoodKeurig system have clearly defined responsibilities, each independently existent. The user does not need to know how a Keurig machine works whatsoever. This is because the GoodKeurig system focuses on providing a well-defined public interface, resulting in code that is easier to read, reason with and change in the future.
If we are dedicated to writing high-quality code, we must realize that each system, whether composed of code or not, is comprised of distinct, separate units with respective responsibilities. We must design software systems in a manner that reflects the key objective of the unified whole of units. As contracts and interfaces serve as the requirement list an implementation must fulfill, the resulting interface must reflect the intention of providing the user the ability to know what a system does clearly. The system must accomplish this at a carefully designed higher level, rather than accidentally burdening the user by requiring knowledge of matters that seem better delegated to the responsibility of a consumed module. This abstraction of complexity results in clearer, more sophisticated software systems that are easily adaptable to changes in the future — changes that will inevitably arise.
The next time you use something as simple as a Keurig coffee maker, recognize the intricate details each unit of the system encompasses and the straightforward interface that has been provided for you — basking in the opportunities possible to provide the same mechanisms to users in the design of your software systems (or reusable water bottles, of course). You may just begin to realize and appreciate the abstracted complexity of all that surrounds you — units, interfaces and systems in all.
(With helpful advice from Anthony Feldhake & Zechariah Schwerman)