A few years ago, I underwent the stressful task of moving my family about 240 miles to another city. Of the many logistical issues I tackled, one of the most interesting came near the end of the whole process. Alone (my wife was out of town), I needed to transport three vehicles (a minivan, a sedan, and a motorcycle) to our destination — a four hour drive each way — and I had a five-week window in which to devise and execute a solution. I was reminded of the well-known river-crossing logic puzzle. When asked how I would solve this, I responded with, “I don’t know”, or, “a solution will present itself”. I was applying a principle I had been using for the last few years when designing computer software. Essentially, I would not write any code until I had a simple, elegant design that I could quickly and easily process. I did not rush to solve my vehicle transport problem until I came up with a simple, elegant solution. As predicted, I eventually invented a simple solution and the execution was quick and easy.
When I design software, I wait (sometimes hours, or days, or even months) until I have come up with a simple design before writing any code. This requires some degree of patience and maturity, but often pays off in the long run. By ‘pay off’, I mean reduced maintenance time: easy to extend, easy to understand, easy to use, etc. In the following paragraphs, I will explain how I do this and why I think every software developer should adopt similar strategies.
As I patiently design software that meets my standards, I have an internal metric to know my design is correct. I know that correctness is a subjective measure, indeed there are often multiple correct designs, but I assert that a simple design is a correct design. I use the metric that if it requires little-to-no mental exertion to process my design, then it is simple enough to put to code. As I explain this concept to others, I state that it should take less than 30 seconds to either mentally process or verbally explain the design. That is my 30-second rule. The key element of this rule is simplicity. And the simpler the design, the simpler it is to explain. If I start writing code before I reach this level of simplicity, the amount of time it takes me to finish the solution is often longer than if I had waited until I had come to a simpler design.
The key to this is encapsulation; having single elements related to single functions. You should never overburden your code with logic that should be performed elsewhere. The question arises: how can you know when and where to draw those distinctions? Here are a few exercises I use.
Devices should have one function. I once gave away an all-in-one printer because I was so annoyed with it. When I tried to scan a document, the device wouldn’t allow it because it was low on yellow ink. Forgive my ignorance, but I don’t understand what yellow ink has to do with scanning. (I confess that I briefly considered driving to Epson headquarters and throwing the all-in-one at the building.) I subsequently bought a scanner and a printer; two separate, single-function devices. I have not had a problem since.
I find this pattern pervasive when I purchase devices that do more than its original, intended purpose. The device becomes a ”Jack of all trades, master of none”; mediocre in many functions, having no expertise. Software is similar. The more compartmentalized the code, the easier it is to be expert in its functions.
Take, for example, a company that produces a specialty soda. Of the many functions needed to perform (ingredient acquisition, mixing, cooking, bottling, packing, shipping, accounting, etc.), can you envision a single person doing it all? Does your code do it all? Can it be broken up into smaller, more-focused tasks?
Packing and shipping are akin to serialization and transfer protocols. If your code does both serialization and transfer, what happens when you move to a different serialization library, or a different transfer protocol? Or think about gathering ingredients and mixing them (gathering data and processing it). These are two separate tasks in real life, your code should mimic that separation.
Picture a little daemon that literally performs the task inside the computer. If it is overburdened, or needs to specialize in more than one operation, consider sending it some help. In short, if your code can be broken up into smaller pieces, then it should be.
Don’t just think how to implement the code, think how it will be used. Put yourself in the seat of a user who knows nothing about the inner workings. How much about the system must they learn in order to use it? The reality is that users of an API need only to know about the API, not the underlying system. If I want to order specialty soda (using the example above), I should not have to know if Anna or Bob does the actual mixing, or what machine bottles it. Those parts of the API should not be exposed to me. I may need to know about the shipping company, but only because they will be showing up at my place to deliver the soda! Consider very carefully how much the user needs to know to access each part of the system. Don’t require them to learn more than what’s needed.
If you find yourself in this situation, you missed something. Think about it. This is an impossible situation, you are dead-locked. Occasionally, I do find myself in this situation, but there is always a common sub-element of both X and Y that I could extract. Thus, “X and Y are interdependent” becomes, “X and Y both depend on Z”. What is Z, you ask? If you look for it, you will find it.
Applying these principles to every level of the software is what I call a Deep API. When each class, functions, library, etc. has a thoughtfully-designed, simple interface, it is easier to design complex logic. If each subsystem, no matter the complexity, has a simple, easy-to-understand interface (accomplished by the 30-second rule), then getting multiple pieces to work together is easy. You end up with pieces of code that fit together like building blocks. Similarly, I continue to apply the 30-second rule while connecting interfaces to avoid accidentally introducing a complex interface.
At times, I notice that some of my code needs refactored. This usually happens when I need to add a feature or connect logic in a way I did not foresee. I then face a familiar dilemma: I can quickly hack together a solution or I can refactor the code. The latter typically requires time that is an order of magnitude greater than the former (a few minutes vs. a few hours, or a few hours vs. a few days). I recognize the temptation to favor the first option, but experience has taught me the value of the second option. The embarrassment of informing my manager of a slow turn-around is overshadowed by strong, well-written, stable, reusable code.
I relate this long-term investment to a concept taught by Henry Ford. He said, “If you need a machine and don't buy it, then you will ultimately find that you have paid for it and don't have it.” The expensive, up-front cost of improvement reaps long-term rewards. Resist the temptation of the quick solution. Play the long game, the rewards are much greater.
I don’t pretend to have the answer to all the world’s design problems. You may read my comments and think, “That won’t work in my code.” That’s fair. I don’t know everybody’s unique situation, I can only confirm my own experiences. Every time I have been patient and applied the principles I set forth, I have been able to develop a simple interface. However, I strongly advise against discounting a simple API for your own code. By doing so, you risk the embarrassment of a junior colleague coming up with one for you.
These principles are general and can be applied to most, or all, code designs.
I chose a convenient weekend and drove the sedan to my destination and took a bus back to my original location. Then, I installed a hitch on my van (which I had planned to do anyway), rented a trailer (one way), and mounted the motorcycle on the trailer. The van had plenty of space for the rest of our possessions and I drove it for the last leg of my trip. Simple, easy.
Brian has written computer software for Expert Consultants Inc., Uber ATG, Aurora Innovations, Inventory Shield, Refferal Reactor, and FlyRealHUDs. Brian also teaches a course on computer gaming engines for Johns Hopkins University's Whiting School of Engineering. He currently resides near Pittsburgh, PA. You can contact him at brian@deepapis.com.
Brian's Design Principles