Why Clean Code Isn’t Always the Best Code
Early in a developer’s career, we are exposed to a number of principles which are considered hard rules by which one must abide. But programming, like any act of creation, is fluid and ever-changing.
These hard rules confine us to a limited set of things we can do and a lot of what we can’t. And while constraints can be valuable tools for producing quality work, they should not be prioritized at the cost of having a maintainable codebase.
This is the basis of one my favorite talks by Dan Abramov, a core maintainer for React, called The WET Codebase.
One of these rules developers are taught is a common practice called DRY (Don’t Repeat Yourself), which is prevalent in all of coding. It is drilled into every developer’s head that duplicate code is bad code. As Dan puts it:
“You’re not supposed to copy and paste code because it creates a maintenance burden.”
To a certain extent this is true; you will quickly find that maintaining 30 copies of the same thing across any modestly-sized project can lead to mistakes.
You can only keep track of so many things in your head (Ed. note: except me… my mind is infinite!!).
So instead, what we do is create “abstractions”, which means we move those repeating sections into segments — small reusable bits of code — that can be applied anywhere they are needed.
Problem solved, right?!
Now we don’t have to keep track of 30 instances of the same code. If the project lead realizes a feature needs changes, we simply need to update our segment, and any related pieces are updated instantly.
Development time decreases, the lead engineer doesn’t have to spend hours reviewing new changes, and the client sees a faster turnaround time.
It’s a win-win for everyone.
Hmm…But what happens when one of these segments needs additional functionality? A one-off, if you will.
Our magical fix is no longer so simple. So what can we do to rectify this?
One option could be to make another segment that is a variation of the original with some added special cases. But we can’t have that, we’re running a DRY dev shop here, not an assembly line.
So instead, let’s add those special cases to the one single, and simple, code segment.
This keeps the codebase small, moves complex logic only where it’s relevant, plus we get the added benefit of keeping the segment tidy and less prone to bugs.
We just have to keep track of these special cases.
You can see where this is headed — our problem began a while back.
By strictly adhering to the DRY principle, we’ve painted ourselves into the proverbial corner. If we overcomplicate things in the never-ending quest of keeping it generic, we force ourselves to keep track of multiple guidelines across various sections — — some sections do A, others do B, etc.
In the end this requires more mental energy to keep track of. While we could offload some of these special cases to our segment, we’ve now opened up the possibility of introducing bugs that will inevitably infect a number of other areas of our project.
So we quickly begin to realize that we can’t fix the problem of painting ourselves into a corner by changing brushes.
We need to solve the problem before we even start painting.
A bit of duplication can prevent any of these issues from arising in the first place. Dan continues on this point but reminds us that:
“duplication isn’t perfect in [the] long term, but [the] wrong abstraction is also not perfect in [the] long term; […] we need to balance these two problems.”
And we do that by inlining our abstractions.
This would be like writing a paragraph and putting in a block quote instead of moving the paragraph into a new document.
Like we mentioned before, code is fluid, projects change, and backing ourselves into a corner for the sake of enforcing a hard rule is setting ourselves up for failure.
Best practices shouldn’t be hard rules, but rather guidelines we should apply mindfully. This could really apply to any larger mission as business and technology continue to evolve.
So what should we do then in situations like these where we don’t have the ability to peer into the future or move back in time?
Well, we can plan a little better. Let’s weigh the pros and cons of abstracting a functionality.
Benefits of Abstractions
Focusing on intent
The whole of computing is made up of a myriad of abstractions. By further abstracting a functionality we can focus on specific layers and better build and improve upon our codebase.
Things also become more modular, which has become the industry standard for a reason. Reusability allows a newer member of a team, for example, to come in and add the functionality elsewhere with less effort.
Avoiding some bugs
By encapsulating a functionality to a single module, we’re able to quickly diagnose a bug and apply a fix that will cover a wide area inside a project.
Costs of Abstractions
Like we described above, any changes to the abstraction can have cascading effects on all associated components.
And that’s when we find ourselves tracking multiple components to avoid introducing bugs everywhere. This accidentally increases the chances of bugs.
Dan said it best: “We try so hard to avoid the spaghetti code that we create this lasagna code where there are so many layers that you don’t know what’s going on anymore at all.”
And so we create this slippery slope. Once you start down this complicated path, every change makes it harder to go back and further complicates our “cleaner” solution.
What we ultimately want to do is make decisions like these responsibly. There also isn’t a need to strictly abide by a solution; remember, code is fluid and our solutions should naturally adapt with it.
Test code with concrete business value
Write tests for your components, not your abstractions. If the rule needs to be modified or undone, you won’t find yourself rewriting tests.
Remember, the idea is to keep it agile.
Delay adding layers
Restrain yourself. Not everything is in need of remaking and/or optimization. A good rule of thumb is to abstract when you notice you have the same code in at least two other places.
Be ready to inline it
If the abstraction doesn’t work, don’t force it.
And if you do find that a segment that worked in the past does not really need to be an abstraction, don’t be afraid to go back to what worked, loosen the rules a little and allow a bit of duplication.
In the end, code is measured on three principles:
- Does it work?
- Is it fast?
- Is it maintainable?
Notice that “Is it fancy?” or “Does it have at least 5 layers?” is not in there.
Good code should be easy to read and maintain a year after you wrote it. By allowing it to adapt you will save yourself a lot of time and effort.