“Good enough” code is just not good enough
My colleague Vedran recently stated that “it’s OK if your code is just good enough” in an article that was both well-received and well-debated. While he makes some good points, I’d like to share some objections.
The article provoked a heated debate on Hacker News, Reddit, and among my teammates. I was annoyed by it (the article, not the debate) and hastily proclaimed I would write a critical response by tomorrow. This proclamation was uttered in front of many people and, to make matters worse, written in a public Slack channel, so there is no going back.
And here I am now, in my room at eleven o’clock in the evening: a hothead writing an opinion piece on a highly subjective and controversial topic to be read by thousands of people.
Why did I make such a silly mistake that led me to this unfortunate predicament? The answer is simple: I have spent most of today and yesterday working on a project I do not usually work on. It is a project filled with what Vedran would call: “just good enough code”. Working on that project left me feeling miserable and annoyed.
“Good enough” is really not that great
For those of you who haven’t had the chance to read the article in question, it states that good enough code is “a nice middle ground between implementing a feature fast and maintaining the code quality“. According to the author, some forgivable shortcomings include unnecessary levels of abstraction, some unclear naming, some misuse of exceptions, some code duplication, and a few larger functions and classes here and there.
My first objection to this “good enough” mantra is that it promotes a culture of mediocracy. It preaches that it is OK to settle with barely adequate. This kind of attitude results in systems that inspire people to coin jokes like:
“The difference between a tech enthusiast and a software engineer is that everything in a tech enthusiast’s home is wired to the internet and controlled over a smartphone, whereas everything in a software engineer’s home is entirely mechanical, except for a washing machine from 2000, but he keeps a shotgun close by in case it started to behave oddly”.
My second objection to the definition of “good enough code “is the notion that “very good code” is so hard to achieve that it is not worth the trouble. I will challenge these notions further down, but before I do that, I’d like to make a few remarks on software projects in general.
Software projects do not exist in a vacuum
They are at the center of an ecosystem of stakeholders, and they are designed to satisfy their needs: the need of the customer for an affordable and user-friendly feature set, the need of the entrepreneur for a multimillion-dollar income, the need of the developer to earn a competitive salary by working with bleeding edge technologies in a collaborative, fast-paced, dynamic environment; to name just a few.
It might not seem that software design details have a huge effect on the lives of the ecosystem’s inhabitants, but that is not the case.
I’ll put this in context with an imaginary take on a classic game, Duck Hunt. Let’s call it Duck Hunt Extreme, a software that enables hunters to hunt from the comfort of their homes through digitally controlled weaponry.
“Unnecessary levels of abstraction”
One of the product manager’s responsibilities is to gather customer feedback and translate it to feature requirements for developers:
“I would like the application to have support for hunting ducks with a chainsaw,” a customer optimistically makes a reasonable request.
An enthusiastic product manager asks developers for an estimate.
“Well, if it weren’t for these crazy levels of abstraction, we might give you an honest estimate, but the code is hard to understand and reason about, and consequently, it is hard to modify. The best we can give you is an estimate about when we will be able to give you an estimate,” the developers respond.
The customer’s optimism and the product manager’s enthusiasm are significantly curbed. These supposedly harmless, unnecessary levels of abstraction increase cognitive complexity, increased cognitive complexity introduces comprehension latency, comprehension latency delays feature delivery and delayed feature delivery leads to customer dissatisfaction – a path to the dark side, that is.
“Some unclear naming”
Intellectual labor that requires strong concentration and careful thinking is best done in peace and quiet, which is why modern software development is often performed in loud, open-space offices filled with chit-chat and background music. Since software development also requires collaboration and carefully performed reviews, it is common practice for collaborators to work on unrelated tasks in parallel and interrupt one another instead of doing the tasks together, one task at a time.
A developer stares at the screen with his head wrapped in his hands, like a chess player hovering over a chess board. Surrounded by noises of irrelevant conversations and other annoyances, he tries to concentrate on the code of Duck Hunt Extreme and ponders, “What does this method do? The name is ambiguous. I will need to read it line by line to figure it out … “.
Minutes pass; “Oh, I get it, now I need to choose the right name for the method to capture its intent correctly in order to prevent other people from going down the same rabbit hole”; other changes are made, and hours pass.
“Oh my, another pull request,” another developer sighs as she leaves her work to review what her colleague has done. She wonders why they aren’t doing mob programming and reviewing code continuously.
She wonders why her colleague renamed the method and tries to determine whether the change is worth checking before going on to look for objections elsewhere. And so, as time passes, the collapse of the solar system slowly approaches, the customer is still waiting for his chainsaw, and the ducks are safe and sound.
“Few larger functions and classes (nothing too big) and some code duplications”
Most novice software developers are haunted by imposter syndrome, doubting their knowledge and skills. In contrast, many senior developers suffer from the curse of knowledge: the assumption that others share their background knowledge. This problem is sometimes tackled by leaving the novices to do the tasks on their own while seniors work on hard problems, creating even more background knowledge and increasing the skill gap.
“Hm, chainsaws… Where should I add this change? It seems to fit inside this big function inside this big class. I guess I could just copy and slightly modify the existing code in another class. I better not try to refactor this to a simpler solution since it was written by people far more experienced than me, and my change does not seem to make things much worse,” a junior developer wonders as she commits her changes, hoping that the senior dev will review them thoroughly and give her feedback.
If a senior developer fails to do what the junior hopes for, another avalanche will start to form at the top of the hill. I do not feel I need to spell out how this approach will play out in several years. You do not need to work long in the software industry to realize that the slippery slope is not a fallacy.
“Misuses of exception here and there”
SRE engineers are a software company’s Night’s Watch on the Wall. They are software engineers with a wide technological skillset and broad knowledge of the service they are sworn to protect. They monitor the systems and prevent or shorten production outages and service degradations.
A sleep-deprived SRE engineer groans as he reaches for his pager in the middle of the night, having received a wake-up call from an automated alerting system.
“How in the hell did the ducks get control of the nuclear weapons?!” he screams in disbelief as he reads the error logs filled with misused exceptions.
“Not funny, not funny at all, and highly unprofessional. Who throws such exceptions!?” he thinks as he returns to bed to catch a few hours of sleep, as an uncomfortable conversation is due on the incident postmortem meeting tomorrow.
The lose-lose situation
Scenarios like those outlined above are more likely to appear around a code base that settles with good enough code. Such code can potentially hurt everyone involved: the customers, the programmers, and the rest of the company. Striving for excellence is not just a matter of professional pride; it is necessary to prevent the endless cycle of suffering.
I am slightly sleep-deprived as I write these final lines, and I feel like a manic street preacher yelling at the indifferent passers-by: “The sins of the past will come to haunt you! Tech debt must be repaid!”
But here is something I observed and thought long and hard about, and I stand by it: the exceptionally good code can and should be achieved, and it is a goal worth pursuing.
Exceptionally good code is not a myth
There is a set of technical practices recommended by highly experienced engineers. When these practices are combined with frequent and honest communication with the customers, they lead to stellar results.
Some of these are worth your consideration:
- Pair and mob programming to create a shared understanding of the project and to share technical knowledge and skills
- Continuous integration to catch and fix errors early
- Continuous delivery to ship the features quickly and safely and getting customer feedback as soon as possible
- Test-driven development to make progress in safe steps while continuously making the code base more understandable and adaptable to future change
This is how we achieve code that is better than “just good enough “: By listening to our elders, the original Agile manifesto signers, and others who have been doing this job for several decades.