All right. Let’s get the obvious thing out of the way right up front.
One could easily argue that code quality (and poor code quality) is subjective. You use camel case while I use Pascal case, and we could argue about the “quality” of the code in those terms. But I have no interest in that.
So instead, let’s talk about code quality in terms of observable outcomes. You may look at a codebase and, based on your experience, conclude that this unfamiliar code is of poor quality. Who wrote this, anyway!? But you do that without understanding outcomes. Does this code serve its purpose in production with minimal issues? Does the application satisfy its constituents? And does the team consistently deliver features on time and on budget? At best, you can make educated guesses about that information when simply reading the code.
I have a bit of an advantage in my study of codebases. You see, I work as a consultant, specializing in codebase assessments, developer training, and team strategy. So I receive phone calls from clients when the external qualities are demonstrably poor — in other words, when the code has many issues, the application fails to satisfy its constituents, and the team misses deadlines and causes budget overruns. It turns out no prospective clients ever call to say, “Hey, things are GREAT, but will you come take a look at what we’re doing anyway?”
So I wind up looking at codebase after codebase that produces bad outcomes. I assess and analyze these things, and I’ve come away with some properties of the code that invariably correlate bad outcomes. Here are five things contributing to your poor code quality.
1. Mutable Global State
Global state happens when you define variables with the scope of your entire application. That broad scope gives you the “global” part of the moniker, while the idea that you store information in them gives you the “state” part. When you make these variables mutable (able to change), you wind up with mutable global state.
You can find endless discussion about the merits of such a thing, and you’ll often hear global stated called “evil.” I’m going to stay strictly away from any theology or moralizing here and talk about mutable global state specifically in terms of effects. And global state has the effect of making it very hard to reason about your code.
To understand, consider a metaphor. Say that someone blindfolded you and asked you to reason about whether a light in a house was on based on the state of switches in the house. Is the bedroom light on? Well, is its switch in the on position? Then yes. What about the kitchen light? Ooh, that’s a bit trickier, since two switches control that light. But we can still map it out.
Now imagine a light for which every single person in your town had a switch. If you even bothered trying to reason about it, you’d give up quickly, and conclude that the only option is to look at the light. That describes global state. Anyone can change it from anywhere at any time for any reason. So if you want to reason about its value, you just have to run the code and see what happens.
And when developers give up trying to reason about and understand the code, that code’s quality suffers.
2. Code Duplication
The second common theme of tortured codebases is code duplication. Sourcemaking categorizes this as a code smell, and you might better know it as “copy-paste programming,” or, colloquially, “copy pasta.” Whatever you call it, it amounts to taking the same code and reusing it by copying it wholesale and tweaking the results.
The simple act of doing this results in codebase noise. If you copy the same code 10 times instead of abstracting it to a single method that you call once, you have 10 times the code. And more code means more complexity and more things that can go wrong. Codebases that stand the test of time keep complexity to a bare minimum.
But beyond the noise problem, duplication has the effect of inviting errors and creating substantially more work down the line. It invites errors because it invites you to forget to tweak something that you need to tweak. And it creates downstream work because, now, when something about that code must change, you have to remember to make that change in 10 different places. What usually happens is that maintainers inevitably forget one or two places each time, and the duplicated code starts to drift apart with time, leaving 10 slightly different, buggy solutions to the same problem.
3. Carelessness with Dependencies
One could argue that, at its center, software architecture is all about dependency management. I personally think of this as the core and most difficult problem in designing software. Dependency management factors into every class you write, every method you look at, and everything you do. If you look at the SOLID principles of OOP, you’ll see that all of them relate to dependency management, with two directly addressing it (the I and D).
Think of your code and architecture as a never-ending battle against a sort of entropy. By default and without deliberate intervention, your code tends toward spaghetti. So you see poor code quality in codebases where no one carefully considers dependencies.
When discussing code quality, you’ll typically hear people talk about readability. With poor quality code, people who are coming in unfamiliar and attempting to read it will struggle. Poor naming, strange formatting, and large units of code all contribute.
But I want to generalize this a bit and talk about opacity in code. Obviously, hard-to-read code is opaque. But you can also have other forms of opacity as well. Perhaps you have a poor abstraction that’s hard to reason about. Or maybe you have a class with an extensive call graph among its methods, making the path through that class’s logic opaque.
Generally speaking, you want code that clearly communicates both its purpose and intent. Opacity obscures this and leads to poor code quality.
5. Lack of Automated Tests
Yes, you had to know this was coming. Not including it would be like a dentist not nagging you to floss. But I do this with a specific purpose.
Yes, automated tests can help you catch bugs and prevent regressions. And, yes, it’s generally a Good Thing™. But in my opinion, its biggest impact on code quality often goes unmentioned. A robust, well-maintained automated test suite gives you confidence while making changes to your code.
Now, confidence in changing your code does not directly translate to code quality. It does, however, make you much more likely to change your code, including to refactor. You can refactor your code to improve on all of the things mentioned here so far: moving away from global state, eliminating duplication, minimizing dependencies, and making your code clearer. When you make a point to refactor constantly, it means you make a point to clean and improve your code constantly.
Without an automated test suite, people tend not to have this confidence. Instead, they tend to treat the code as an ancient appliance — “it’s kind of working, so, whatever you do, don’t touch it.” And that’s essentially the hallmark of poor code quality.
Avoiding Poor Code Quality Is a Battle
When talking about code quality, it’s easy to seem (and be) judgmental. I spend a lot of time working with teams on lowering the total cost of ownership of codebases, so client developers often assume I’ll look judgmentally at what they’ve done.
But really, the more I do this, the less I judge. In the first place, I don’t know what sort of constraints they’ve faced and how the code got to the state it did. What’s more important is something that I actually explain to them when I arrive. Codebases naturally drift toward messiness unless you put forth a serious effort to stop that from happening. Poor code quality is the default state. So it’s not so much that they wrote poor quality code but rather that, for whatever reason, they didn’t have the time and resources to stop it from rotting.
Adopting that outlook is healthy for teams and for managers. Unless you invest in your team, your education, and your development process, your code will get more expensive to maintain as you go, and quality will suffer. So make the investment and avoid the sorts of traps I’ve discussed here.