Supportable code is flexible and easy to modify because it’s readable and understandable, not just by the developer who wrote the code but by other professional software developers as well.
We’ve seen previously that one aspect of “good code” is that it’s easy to change. When I ask developers what makes code easy to change, they usually say things like it’s well documented, it uses intention-revealing names, it follows a consistent metaphor, and so on. These and other things can definitely help make code easier to understand and easier to change.
Are there guidelines developers can follow that show them how to make code easier to change? I believe the answer is a resounding Yes! We’ve already discussed several principles and practices that help make code easier to change. But let’s ask the converse question, because sometimes knowing what to avoid is just as important as knowing what to strive for.
What makes code difficult to change? Are there things developers do that make their code hard to change later?
I believe that some commonly accepted developer practices are actually impediments to creating changeable code. Changeability impediments can become roadblocks for easily working with code. Many of these impediments are little things, and individually, they may not be a problem. But when created regularly, as a practice, such impediments can seriously slow down development.
Let’s start by looking at some common developer practices that can be impediments to change. This can get a little technical, so nondevelopers, please bear with me.
Here’s my short list:
The more one piece of code “knows” about another, the more dependencies it has, whether it’s explicit or implicit. This can cause subtle and unexpected problems where one small change can break code that’s seemingly unrelated. As soon as one piece of code “knows” or depends on the way another piece of code is implemented, it can become difficult to change without causing the other parts of a system to break.
Inheritance is an important and valuable part of object-oriented languages, but it can be easily overused and misused, linking together unrelated issues, producing steep inheritance hierarchies, and causing maintenance problems.
When key abstractions are missed, so are commonalities between two or more behaviors. This tends to introduce redundancy and needless complexity that make code harder to work with. Concrete implementations are more difficult to change or add new variations to in the future.
It was considered an efficient practice on constrained systems to copy and paste code inline, as needed, rather than wrapping the code in its own method and calling it. But this can make code harder to read while it also introduces redundancies. Today, most compilers can optimize indirect method calls away, and extracting code into their own methods gives us the opportunity to improve readability by providing meaningful names for them rather than preceding the block of code with a comment.
The way you handle dependencies is also important. If you do not separate them correctly, you can end up coupling multiple issues together that don’t need to be coupled. This makes it difficult to break those issues apart later.
This was a difficult one for me to understand at first, but it turns out that this is one of the most important things not to do for writing extendable code—code that’s able to be extended with minimal effort.To instantiate an object, you need to know a great deal about it, and this knowledge breaks type encapsulation—users of the code must be aware of subtypes—and forces callers to be more dependent on a specific implementation. When users of a service also instantiate that service, they become coupled to it in a way that makes it difficult to test, extend, or reuse. This will be discussed in more detail later in this chapter.
These are just a few impediments to easily changeable code. They can be small things that when done occasionally can be no big deal. But when done repeatedly throughout millions of lines of code, they can add up to big problems.
I have a friend who is a hardware engineer. He designs chips. He says, “Software is too forgiving,” and he is not paying our industry a compliment. When he designs a circuit, he knows that one little mistake can invalidate his entire design and saddle his company with hundreds of thousands of dollars in re-fabrication costs. As a result, he’s extremely careful—even obsessive—in his practices, checking and rechecking his designs before passing them along to manufacturing. He thinks that most of us software developers are undisciplined, shoot-from-the-hip slobs who focus on the wrong things and create little messes in code everywhere we go.
I can’t argue with him.
The problem is developers can get away with being a little sloppy—but only to a point—and most developers weren’t taught to be highly disciplined when writing software. The kinds of programs they wrote in school over a semester were relatively trivial and small compared to the kind of enterprise systems they find themselves writing professionally.
You can get away with being sloppy on small projects but at some point, when a project gets big enough, the results of our cumulative sins inevitably catch up with us.