Cover photo by Dan Wayman on Unsplash
Last week, Dan Abramov posted a very personal and humbling blog post entitled Goodbye, Clean Code.
I saw a tweet about this in my timeline and, being a long-term proponent of “clean” code, TDD and things of that ilk, I was naturally concerned. Here’s what I replied with.
I dislike Twitter because it’s so hard to find any nuance to arguments. So in this post I’ll explain what I mean by human code.
It’s easier to blame code than it is ourselves
I think it’s wonderful that Dan is blogging about deeply personal experiences in his career.
Many programmers who become team leads will have had a similar experience to the one he is describing. That time when your colleague wrote some code that you didn’t like so you rewrote it, because you wanted your codebase to be the best it could possible be. Then all hell broke loose. You offended your colleague, you made it awkward for the rest of the team, and your boss had to step in and sort it out.
At some point it dawns on you that being a team lead means leading from the back. That your team will only ever go as fast as the slowest person on your team, and your job is to help everyone level-up, not just yourself.
There were a bunch of people who replied to Dan’s tweet about his blog post with the same comment. Isn’t it interesting how common this experience is in tech?
Human code
We are getting to the crux of what I mean by human code. It is code that has been written with a people first approach.
I am unsure who first said the following expression, but I first heard it at the SoCraTes 2019 unconference. (Please let me know who said this, if you know!)
The two hardest problems in computer science are people, and convincing people that people are the hardest problem in computer science.
Isn’t that a wonderful saying? In my work as a software consultant helping businesses solve their software problems, almost always the biggest problem I see is interpersonal issues that stem from disagreements about project direction and structure.
Clever code
Another problem here is what does “clean” mean? It does not mean the shortest code, or the code with the most intelligent abstraction.
Take the acronym DRY (Don’t Repeat Yourself), which people misunderstand all the time, and then invent other acronyms like WET or AHA. We don’t need these acronyms. DRY is fine. It’s a topic that deserves a whole blog post on its own, but for now let me just say that there’s another term that helps understand the issue.
It’s the term clever code. I like this term because it brings to mind the image of the lone wolf, “10x” coder who is trying to prove themselves better than everyone else around them. This behavior is toxic. Clever code is toxic because it takes a disproportionate amount of time to read and maintain it. Clever code is a ticking time bomb.
And by the way, there’s no judgement here from me, because clever code is my default mode when I’m working alone. But I write much better code when I’m pairing with people. Working with others is a great way to block clever code from ever appearing.
So that is where the idea of clean code over clever code comes from.
But...
Clean code is dirty code!
Many of us in the software crafters community decided long ago to stop using the word “clean” to describe our code.
The problem is that by saying “clean” we are implicitly stating that some code is “dirty”. This can be very shaming for people. Particularly for beginners, it’s an example of the kind of word that leads to imposter syndrome, and a feeling that your code just isn’t code enough.
If you’re following the principle of human code then you want to avoid anything that could possible trigger negative responses in your colleagues, and that includes using the word clean.
It was Tobias Goeschel who first introduced me to the term clear code as an improvement on clean code. When I discussed this post with him, he reminded me that there’s a further problem with clean, and that’s the illusory binary distinction of clean vs dirty. All the code we write involves trade offs, and it’s not helpful to believe that there is always one right way of doing things.
Just like how DRY code isn’t a binary thing either. People hate on DRY because they believe it is a binary thing—it’s either DRY or it isn’t—but in reality DRY is just a gentle nudge in the right direction of code quality.
Yes, I care about code quality. But I also care about people.
Let’s stop judging each other. Let’s work together to create awesome software. 🤗
Top comments (49)
Yeah, honestly, I once got sucked into the clean code rabbit hole, and it nearly ruined my ability to program. I'm not exaggerating. When you become preoccupied with writing clean code obsessively, you redirect your attention from solving the problem at hand.
Nowadays, my process is simple:
Write the code that gets the job done, no matter how ugly it may be, unless a better alternative is reasonably doable without investing more time.
Refactor the code to make it easier to read and/or more performant.
That's it. I think that's all you really need.
I hate how it's so simple, yet I still find myself in that "rabbit hole". 🤦♂️
Yeah, I feel ya. And I know exactly how debilitating it can be when you spend hours searching for a "clean" solution to a problem, when you know you're capable of coding up a "dirty" approach in less time.
More often than I'd like, I'm too pedantic for my own good. It ruins my productivity. I should really work on that.
It puts into perspective why people "iterate" on some code. Initial implementation is expected to be dirty, but that's okay because "iteration" is a thing.
Perhaps that's where my pernicious obsession with "clean code" comes from: I would believe that "iteration" takes up more time than if I had just done it properly the first time. In reality, of course, I end up spending much more time than needed.
I can't quite recall what exactly, if anything, got me into that mindset to begin with, but it happened. Ngl, I'm still kinda dealing with it. There's nothing fundamentally wrong with writing clean code, but I think it's definitely a distraction and should only be a secondary concern, once you have something that at least works. Most of the time, people jump the gun and head straight for the Clean Code promised land.
I agree with your approach which involves those two steps. But I do the refactoring if there are enough time to do so. Currently I work alone on both front-end and back-end in a project that has a lot of modules; sometimes there aren't enough time to refactor certain codes in my project.
It's easy to drive guidances like DRY and "Clean Code" much farther than they were meant to. After all, in his book "Clean Code", Uncle Bob wrote:
Not quite. Firstly this has nothing to do with being fragile or easily offended, it’s got everything to do with the skill of being able to communicate feedback about quality issues in a way that keeps the recipient of that feedback on board with the message you’re trying to get across. Admittedly this skill doesn’t come easy to many developers, since it’s one of those hard people skills, but it’s an important skill that mature developers will nurture and develop over time.
Secondly, having taught many bootcamp grads as a coach and mentor, I’ve always found that saying ”This code isn’t clear to me” gets a much better response than ”This code isn’t clean”.
One is a statement of fact that puts both teacher and student on an equal footing. The other is an unnecessary judgement.
I'm so glad you highlighted this difference with an example from a human:human interaction 👏
I think that's something we often forget, that these terms and principles are meant to help us communicate more effectively with the human readers of our code, not (or not primarily) with the machine and our internal ego.
I really like that perspective. Thank you for sharing.
I definitely like the idea of switching 'clean' code to 'clear' code. Another term I've been using is semantic code. You see semantic used a lot when referring to HTML, and why using a div for everything is not helpful. I also think the same applies to our code.
When code sucks, we should not say it doesn't. No need to be mean, but politeness cannot be used to erase the truth. Besides, how can a beginner learn to code better if we are too scared to point the problems in their code? To be human is to err, learn, and do better. Blocking critisicm because someone feeling might get hurt is doing them a huge disservice.
I don’t disagree. I used to feel this way. It’s just that at some point I realised that at this low level of line-by-line scrutiny it doesn’t really matter. A more important axis is time. Does the codebase improve over time? Does their coding style ‘improve’ over time? I say improve in quotes because what I really mean is does their style become more like my subjective idea of style over time?
I’d rather teach people to code like me by showing them my code than by spending time/energy poking holes in theirs.
I get your point, and I agree about style - Style is a matter of personal preference. There are also different "schools of thought" each with their own, respectable, set of principles. We should not fight a war to "convert" people with other principles to hold our own.
But there exist code that is objectively bad. It's not a matter of style, it's not a matter of different principles, it's a matter of lack of principles.
When code cannot be read by human beings, when it misleads and hides the developer intent, it's an objectively hard code to maintain. When code contain wrong abstractions, when it is littered with unnecessary code duplication, when it contains huge spaghetti methods, when it is riddled with hidden dependencies, when it is so brittle that touching some part breaks some distant completely irrelevant other part... It's just plain dirty code, filled with bad practices.
Just like there are good principles, there are also bad practices.
And we should not be afraid to say it.
@sinapis I agree 💯 We should not be afraid to criticize unmaintainable work, and if you can't fix it, I shouldn't ship it.
But not all 'objectively bad' code is worth my time to criticize. It's taken me a while to come to this perspective, and I still routinely lose it (and truthfully, sometimes I willfully forget it). Giving criticism is challenging to do well, because the goal of criticism isn't to improve the current iteration: that's impossible, it already happened. Your goal with criticism is always to improve the next iteration, and the next and the next; focusing too tightly on the code can limit the usability of your feedback, but focusing too broadly on design principles isn't likely to stick in anyone's head for long.
Additionally, iterations and feedback are happening in the larger context of a team's current sprint of work. Your personal priority may be to help your team produce elegant, quality code; but your real priority is working together to produce the best quality you can within your given constraints. Sometimes the pull request has also gone through 4 iterations already and the sprint is nearly over and Sales needs you to do that thing and so you say let's just get this to a 'save point' so we can continue iterating when we're back in town next month.
I love it!
Human code > clear code > clever code > dirty code
if It gets the job done, then there’s really no point in arguing about it.
Unless there is some job you think is missing, verbosity, performance, testabillity, future proof?
If that’s the case it can be explained in an objective unaccusatory tone.
I often leave optional‘ comments in PR‘s where I point out verbosity semantics, and other nice to have things, but I make it clear they are optional.
And then I say we’ instead of you when I point out things I want changed.
Saying we’ and shaping change requests as questions or requests is a great way to review in a more casual way, bonus points for remembering to include a why or a link that describes what you are getting at.
Ofc. this goes both ways, as PR makers we gotta remember, this is not a war, the reviewer is there to help. Regardless of how they might accidently come across.
Thank you for this.
It’s probably just a convenient excuse to be lazy, but I very rarely comment on PRs. Only if I spot an obvious functional error or code that will perform badly. Anything else can be improved in a later PR, if necessary.
I prefer to pair/mob when I can so that code can be improved at the moment it’s written.
As with most things, there's all sorts of intersections and cases to consider on this spectrum and what is refactoring for simple clarity/cleanliness versus refactoring for good architecture and reduction of tech debt.
We've all inherited a code base, or written one (like me), where the authors leaned towards 'it works' and wrote code that quickly achieved the intended goal at the time, and then was grown to handle various new requirements. At each iteration a new 'solution' was derived to add widgets 1, 2 and 3 and the code marched forward.
When we, those who come after, then take our turn, we find ourselves deep in the technical debt that was left behind. In those situations, it feels certain that a refactoring like the one Dan Abramov describes would have made our job easier. At some point, someone with enough context and sense for the direction of the project could have reorganized the code in a way that is demonstrably better, that establishes a pattern for extension that we, the people having to add
widget4
, would be able to quickly grok and build upon, leaving a well ordered and easily understandable chunk of code for the next developer.Then there's code like Aleksandr below proposes - code that was written quick and dirty, but then thoughtfully refactored for readability or performance - better method names, smaller methods, fewer allocations, added some caching, etc. And this can be great, all that is necessary, except sometimes it's the overall design and structure that becomes limiting regardless of how clearly this chunk attains the localized goal - it may do the same thing almost as well as other code that was written somewhere else or perhaps it could have been segregated or composed in a way that made it easier for similar components to reuse in the future, or it could be clear and quick to write, but it doesn't fit in with patterns established everywhere else (hey, this thing is talking directly to the database, but we have a repository for that!)
Pernicious smells and debts can creep in this way. You can find yourself with a whole lot of code that does what it's supposed to do, and is clear in that purpose, but still is difficult to extend without becoming an expert in the entire system. Or you can end up making improvements that only help there instead of everywhere like there.
There can be value to slowing down and defensively refactoring out repetition across a code base or being more thoughtful in the design, balanced against over engineering. That's the art, choosing enough good design that you leave the hints and the intention for the expansion, so when it's time to grow the system those people looking in with fresh eyes can see where and how it can be most easily extended.
For example, if I can see that a system I'm building has a clear place where a chunk of functionality could be abstracted out, say in a strategy pattern, even if there's only one such strategy now, I may still elect to take a small amount of time to build in this pattern now, because I have the context and understanding to know that this piece is replaceable. Later someone else looking to build a new feature may be determining how difficult it will be and come across this 'hint' and think - "oh, that piece there is 'swappable', which means my feature can simply implement a new strategy there and away I go". It may mean the difference between a feature being 'feasible' or 'a lot of work'. Though on the other hand, someone may never come back to this code and need that extension point, so it's a judgment call - but my feeling is that over time, without refactoring for extension or pulling out the commonality, systems become hard to reason about how to extend them and about how hard that extension will be to implement.
Hey, Daniel!
Thank you for sharing this with us.
So true!
Be pragmatic. A lot of code does not survive the first year of its existence. Writing clean code at least gives it a chance but sometimes getting things up and running is more important than over-engineering it (which is what clean code fetish can escalate into).
I like to think in terms of disposable code. Lets be real, if it's frontend code, it's going to be replaced in under 2 years. Also most of your team will have moved on and be replaced with a few senior full stack guys that have not yet reached the ripe age of 28 and will be likely to want to try whatever is fashionable. Exaggerating here of course but front end code has a short shelf life in my experience. I don't care about how clean it is but it should be testable, robust, and work as advertised. These three together typically amount to the same thing but they are a bit easier to reason about than "elegance" or whatever.
For backend code, do something simple and conservative that is likely to be still valid in five years because a lot of backend code never gets retired. So doing it right is more important. If it gets retired, it's because something went wrong. Replacing the backend in a company tends to be disruptive and risky for the company. A lot of money in our industry is in patching up existing backends.