This ninth post in the Making Software Quality Visible series shares Michael Feathers’s definition of “legacy code” and his concept of “seams.” This then dovetails with Scott Meyers’s “most important design guideline.”
I’ll update the full Making Software Quality Visible presentation as this series progresses. Feel free to send me feedback, thoughts, or questions via email or by posting them on the LinkedIn announcement corresponding to this post.
Continuing after Individual Skills Development and Code duplication, large changes, and bad excuses…
Legacy code
In Working Effectively with Legacy Code, Michael Feathers defines “legacy code” thus:
“To me, legacy code is simply code without tests.”
—Preface, p. xvi
His rationale, from the same page:
“Code without tests is bad code. It doesn’t matter how well written it is; it doesn’t matter how pretty or object-oriented or how well-encapsulated it is. With tests, we can change the behavior of our code quickly and verifiably. Without them, we really don’t know if our code is getting better or worse.”
Of course, we can also change our code while preserving its behavior quickly and verifiably, for the purpose of refactoring.
Seams
Feathers further explains that “seams” are where we can change the behavior of code without changing the code itself. There are three kinds of seams:
- Preprocessor seams use
#define
macros to rewrite the code in languages that use the C preprocessor. - Link seams use a static or dynamic linker, or the runtime loader, to
change how a program binary is built or run. Examples include manipulating the
LD_LIBRARY_PATH
orCLASSPATH
environment variables (or their equivalents in other languages’ build and runtime environments). - Polymorphic seams rely upon dependency injection to build an object graph at runtime. This allows the program itself to choose which implementations to include—such as test programs using test doubles to emulate production dependencies.
Polymorphic seams are the most common and most flexible kind, as well as the first one we reach for to write testable code. The term is essentially synonymous with “dependency injection.” Preprocessor and link seams aren’t as flexible, scalable, or easy to use, but can work if you have no reasonable opportunity to introduce polymorphic seams.
Note that using any seam successfully depends on the quality of the interface that defines it. The upcoming Scott Meyers quote speaks to that.
See the footnotes from The Test Pyramid on test doubles and internal APIs for some details on the benefits of dependency injection. (Or wait for me to extract the posts about them in the future. If I remember, I’ll add the links here when I do.)
(2023-09-13: Oh look—I remembered! Eventually…)
The most important design principle
Speaking of interfaces, Scott Meyers, of Effective C++ fame, gave perhaps the best design advice of all for writing testable, maintainable, understandable code in general:
“Make interfaces easy to use correctly and hard to use incorrectly.”
To propose a slight update to make it more concrete:
“Make interfaces easy to use correctly and hard to use incorrectly—like an electrical outlet.”
—With apologies to Scott Meyers, The Most Important Design Guideline?
Of course, it’s not impossible to misuse an electrical outlet, but it’s a common, wildly successful example that people use correctly most of the time. Making software that easy to use or change correctly and as hard to do so incorrectly may not always be possible—but we can always try.
Electrical outlets
I first started using electrical outlets as an example in Automated Testing—Why Bother?:
First, we need to understand the fundamental building block of testable code: Abstractions, as defined by interfaces. We create abstractions every time we write a class interface, or a module interface, or an application programming interface. And these abstractions perform two powerful functions:
- They define a contract such that certain inputs will produce certain outputs and side-effects.
- They provide seams between system components that allow for isolation between components.
My favorite example of a powerful interface boundary is an electrical outlet. The shape of the outlet defines a contract between the power supplier and the power consumer, which remain thoroughly isolated from one another beyond the scope of that physical boundary.21 It’s easier to reason about both sides of the interface than if the consumer was wired directly into the source.
In software, problems arise when we fail to consider one or the other of these functions, when either the contract isn’t rigorously defined and understood, or when the interfaces don’t permit sufficient isolation between components. This often happens when we fail to design our interfaces intentionally.
In contrast, the more intentional our interfaces, the more natural our abstractions and seams. Automated testing obviously serves to validate the constraints of an interface contract; but the process of writing thorough, readable, reliable tests also encourages intentional interface design. “Testable” interfaces that enable us to exercise sufficient control in our tests tend to be “good” interfaces, and vice versa. In this way, testability forces a host of other tangible benefits, such as readability, composability, and extensibility.
Basically, “testable” code is often just easy to work with!
21 For a list of electrical plug and outlet specs used across the world, see: https://www.worldstandards.eu/electricity/plugs-and-sockets/
Put more concretely: the power source could be anything from coal to wind, hydro, solar, or a hamster wheel. The consumer could be a lamp, a computer, or a wall of Marshall stacks. The shape of the outlet should ensure the voltage and amperage matches such that neither side cares what’s on the other—it all just works! A fault, failure, or other problem on one side won’t usually damage the other, either. This is especially true given common safety infrastructure such as surge protectors, fuses, and circuit breakers. Plus, you can use an electrical outlet tester as a test double to detect potential wiring issues.
It also greatly simplifies debugging (also sampled from my 2022-12-23 email with Alex Buccino):
-
If a plugged in device stops working, but the lights are still on in your house/building, you can check a few things yourself. You can see if it’s unplugged, if a switch was flipped, if a fuse/breaker blew, or if the device itself is faulty. You can pinpoint and fix most of these issues quickly, with no need to worry about the electrical grid.
-
However, if all the lights went off in your house at the same time, the problem’s beyond your control. Unless you work for the electric company, you should be able to trust that the company will send a crew to resolve the issue shortly.
-
Were the device wired into the electrical system directly, however, your debugging and resolution would be more costly and risky. Also, the delineation of responsibility between yourself and the electric company might not be as clear.
The common electrical outlet is a remarkably robust interface that unleashes enormous productivity every day—imagine if software in general was even remotely as reliable!
Coming up next
The next post will dive into the importance of team and organizational alignment and how roadmap programs like Test Certified/Quality Quest can help.