Writing code that people like to read
Why it's important to communicate reasoning
Suppose you have been tasked to fix a bug in a codebase, and you have been able to trace it to one file. You read through it, hoping that doing so will help you find the cause of the bug. You run across these lines:
readConn := amqp.NewConnection(uri)
writeConn := amqp.NewConnection(uri)
readClient := amqp.NewClient(readConn)
writeClient := amqp.NewClient(writeConn)
This is a bit weird.
Wouldn’t it be better to use a single connection and client?
Or is there a reason for the split?
Of course, since you don’t like leaving hurdles around for other people to stumble into, you decide to look into it.
You start with git blame and git log, but the two connections have been there for as long as the file has existed.
What you do see, however, is the names of the people that made changes to these lines.
One of them has already left the company, so you send a message to the other two asking if they know the reasoning behind the code.
A while later, one replies that they were just renaming stuff. Although they did see the two connections, they assumed it was a quirk of the library. The other - the one that wrote the code in the first place - remembers writing it that way, but they don’t remember why and seem as puzzled as you1.
At this point, you have three options:
You can assume it is a legitimate quirk and spend time digging for the reason why it might be this way. This is the most thorough alternative, but it can take quite a while. And keep in mind that this might not even be the cause of the bug you were working on in the first place! Additionally, at some point you will have to stop looking. If when you stop you haven’t found anything, you’ll have some confidence - but nothing more - that the weird code is a mistake. It could still be legitimate.
You can also treat it as unintended and merge the two connections into one, risking that it actually is intended and you’re reintroducing wrong behaviour. Maybe there’s a regression test that will fail, although it’s not likely: the code was like this from the start, so there’s no regression (i.e. a change back to an earlier broken state) to test for. Relying on automated and manual testing also doesn’t give you certainty. You don’t know what behaviour would be different based on your change so you’re left to blindly test. You could always invest in more thorough testing, but as already mentioned, this investigation isn’t even part of your task.
Finally, you can just give up and leave it. This means that anyone that comes after and is also willing to leave the code in a better state than they found it will trip on the same stone. More importantly, it could cause a future bug or performance problem. You probably could not be held responsible for it because you didn’t write the code, but would you be happy with yourself in that scenario? Would you feel like you’re off the hook? I know I wouldn’t.
It’s not your fault that you don’t understand (most of the time)
There are, broadly, two reasons why we might not understand the intent - if there was any - behind a weird piece of code.
The first and simplest is that we lack knowledge that we are expected to have. We may not have fully grasped the domain or conventions if we joined the project recently, or we may need to spend time getting acquainted with tooling and frameworks. These are all our personal responsibility to address. At the same time, they’re the easiest to remediate: since we’re looking for pervasive knowledge that everyone’s expected to have, any colleague should be able to fill us in2.
The second reason would be that the writer has some knowledge that others are not expected to have, and they (the writer) have not conveyed this knowledge when writing the code. For example, they could have found a bug in an external dependency that temporarily requires a workaround. They could be using a library that has a caveat deep in its documentation. Anyone not deeply acquainted with the library, dependency, or whatever else the weird code relates to is likely to wonder why it is not written in a more obvious way. In large enough codebases, this can mean almost everyone, including the writer themselves when enough time has passed without them having to interact with that piece of code.
Since the reasoning or intent behind the writer’s choice is the clearest at the moment of writing, it stands to reason that that’s the best time to address the clarity problem. As time goes on, the knowledge disappears (either because the writer leaves or forgets), and the only thing that a reader can do is try to piece it together and update the code or documentation so that the intent is clearer. At this point they have most likely wasted as much time as the original writer - or more - figuring out why the code has to be the way it is, and they might not even be certain they arrived to the correct reason.
Even though as a reader we are in an uncomfortable situation caused by someone else, there is something we can take away from it. We are also writing code for others to read, so we might as well try to avoid doing unto others what has been done unto us. So first, let’s examine our expectations and put into words why we feel uneasy when reading in the first place.
Standard code
So how can code be “weird”?
Behaviours3 can be implemented in many different ways. Some will be more correct (i.e. doing what is expected, not missing edge cases), performant, verbose, or complex; others less so. Different technologies or libraries might be used. Formatting might change as well, and even though it doesn’t impact the runtime behaviour, it’s also a factor in our reading process.
When someone with experience in the domain and technology thinks of a behaviour, they are going to have a rough expectation of what the code for it should look like. In other words, in the set of possible implementations they define a subset containing the ones they expect to see. The criteria are, among others, correctness, code complexity (how hard it is to read and understand), and how well the choices made align with the context the code lives in (e.g. technologies, tools and libraries used, consistency with the rest of the code).4
It is worth noting that although there are some dimensions in which we will want to maximise or minimise, our expectation will not necessarily align with our desires. For example, we always want to keep complexity to a minimum, but might expect a procedure to be complex based on the requirements or edge cases it has to fulfill. If it turns out to be less complex, it will most likely make us suspect the correctness of the implementation.
Additionally, “standardness” is a subjective measurement. Every reader will have their own thresholds of weirdness based on their experience - technical as well as in the domain - and style.
So in a nutshell, code is standard to someone when it matches their expectations of correctness, complexity, alignment with the context, and possibly others. Conversely, it is weird when it does not.
We ideally want all the code we write to be standard to everyone that has to read it, provided they have a baseline knowledge. If I’m a Java developer I should attempt to write so that other Java devs working on the codebase see my output as standard, but I don’t have to be as mindful about a Python developer asking why we’re not following Python conventions.
Admittedly, what “standard” is is a very loose criterion that depends largely on context and personal judgement.
Deviating from the standard
Unfortunately, there are times when we are forced to write in a non-standard way. Possible cases could be loop unrolling to improve performance, or temporary workarounds for upstream bugs.
It’s fine to deviate in these cases - what else can we do, after all - but we have to make sure that future readers understand why the weird code is there. This is efficient, because they won’t have to spend time to reach your level of understanding. It’s safer, since it reduces the chances that they are wrong about the reasoning or misunderstand. And lastly, it’s a nice gesture that they will appreciate.
It’s not a purely altruistic action either: remember that oftentimes you will be one of the future readers. If you have been coding for long enough, you’ll have run into code you wrote that makes you scrath your head. In an alternate reality there would be an explanation and you’d go “oh that makes sense, thank you, past me”5.
You can write your reasoning in a number of places, but ideally it should live as close to the thing being reasoned about as possible. The less a reader has to search, the smaller the disconnect from the reading process is. When I code, this usually means:
- Comments, if the weird element is a small piece of code: from one line to a class or file, or package when package comments are a thing.
- The commit body, if the change that the commit introduces contains all or most of the weirdness.
- The PR, depending on what the culture around them is in the organisation.
- External documentation like Confluence or Backstage.
However, you should of course take into account the agreements and habits of the people around you. If everyone writes commit titles like “fix”, “typo”, “implemented feature”, or “pr comments”, then no one will think to check a commit description for information6. You will have to put it somewhere else.
The golden rule
At the end of the day, all of the above can be completely ignored if you just do one thing: put yourself in the reader’s shoes. The easiest version of this is: imagine yourself back when you had not yet started designing the code you are writing (or in the future, when you have forgotten). At that point, if you were presented this code, would you understand it? Where would you trip or slow down? How can you change those parts so that it’s easier on past and future you?
Also, when you actually run into weird code, take some time to think. Why does it trip you up? How could it be written differently? Maybe it’s just missing a comment with reasoning. Maybe the abstraction has outlived its usefulness and needs to be rethought. This closes the feedback loop: the next time you design something you’ll have a bit more experience on what’s nice and readable and what’s quirky and confusing.
Ideally, though, you should think about others as well: you are writing for an audience, after all. How do people around you write? There most likely is a local standard that you should adhere to: it will make reading easier for everyone else. But more importantly, where do they go for answers to their questions? Put your reasoning there.7
Show others your code and ask them if they can fully understand it. What do they ask? If your answer is accepted by them, how could you have written the code so that the question didn’t have to be asked in the first place?
Keep this reader/writer perspective in mind and you won’t have to worry about techicalities like the definition of standard and whether you adhere to it, nor if PRs have priority over Confluence as a place to write your reasoning in. Or rather, you will still worry, but only from the perspective of what is more helpful to everyone interacting with your work. So do it and then listen to the feedback - from others, but from yourself as well. Look at everything with a critical eye. Everyone around you - and future you - will be better off for it.
-
This is only one scenario of many. Maybe the writer actually remembers why they wrote it that way, in which case you (collectively) got away scot-free… this time. ↩
-
I’d like to highlight that this is true for facts that everyone knows. If it’s something that a group of people knew, but now only Mike is left, it doesn’t count. ↩
-
Or features, requirements, or whatever you want to name ideas that are brought into existence through code. ↩
-
People with less experience also define their expectation set, only it contains more possibilities. ↩
-
Or in another reality: “I have no clue what I was meant to say here, what the hell was I thinking?”. But hey, at least you tried. ↩
-
I’d encourage you to write descriptive commits anyway, if only for yourself. ↩
-
If you think the choices for standards, documentation, etc. don’t make sense, challenge them; don’t just do whatever you think is best and hope people follow suit. That is, assuming the culture embraces or at least tolerates challenges. ↩