The current global situation is enough to make anyone sink into a state of depression and helplessness. It has even happened to me, but for entirely different reasons than one would expect. Over the last month, more often than not, I have encountered very strong opinions on so-called self-documenting code. It was either in interviews with potential employees or when reading articles coining the theory that self-documenting code is the only right way to write documentation (meaning they don’t write any documentation at all).
Maybe it is just a misunderstanding
Of course, it is a beautiful thought—we will write code in such a way that is completely self-explanatory and does not require any documentation. Even on this level, it is already quite debatable, but we will come back to that later. However, what is worse is that these arguments (if they can be called anything other than trench warfare) often resort to fouls like “if your code needs documentation, it’s bad”, “if you can’t write self-documenting code, you are a bad programmer”, etc. More than once, I have also come across the idea that for larger projects, documentation makes sense, but for smaller start-ups with teams of up to five people, it is entirely superfluous. It would be the last nail in the coffin if someone rediscovered the 20-year-old agile manifesto and quoted the famous saying “working software over comprehensive documentation”. Make no mistake—this quote does not say “don’t write documentation”, only “don’t write more than what’s necessary”. The fact that many teams did not understand that back then (and some still do not understand it today) is a different issue.
We definitely should be writing easy-to-read code. None of us wants to read something like this.
x, y = Adafruit_DHT.read_retry(11, 1) if x is not None and y is not None: return x, y else: return -99, -99
The same code written more comprehensibly could look something like this.
humidity, temperature = Adafruit_DHT.read_retry( Adafruit_DHT.DHT11, SENSOR_PIN) if humidity is not None and temperature is not None: logging.debug("Sensors read (Temp=%0.1f, Humidity=%0.1f)", temperature, humidity) return humidity, temperature else: logging.error("Error reading sensors!") return UNKNOWN_SENSOR_VALUE, UNKNOWN_SENSOR_VALUE
So, have we won? Is it enough to name the variables well, structure the code, edit a bit here and there and finished? Definitely not! For any code, it is not enough to ask “What does the given code do?”, we also have to ask “Is it doing what it really should be?”. Let’s try a completely trivial example of an algorithm that tests whether or not a given string is a palindrome. This time we will use Java.
public static boolean isPalindrome(String word) { if(word == null) { throw new IllegalArgumentException("Not a palindrome"); } for(int i=0; i < word.length() / 2; i++) { if(word.charAt(i) != word.charAt(word.length() - i - 1)) { return false; } } return true; }
Is this code readable? Yes, definitely. Do we understand what this code does? Yes, it is undeniable. The algorithm first tests whether the input is valid. Then it iterates to the middle of the string (since it is always rounded down to a whole number, we do not have to deal with even/odd length). If it encounters any discrepancies, the result of the function is negative. If the algorithm finishes iterating, the result of the function is positive. So where is the problem? There are several problems. First of all, we could question whether the code should really throw an exception for invalid input. Was it contrived like that? Furthermore, the code does not address the capitalisation of the string characters. Is that correct? Does this mean that the string ELE is a palindrome, but Ele no longer is? Is it correct? What if there is a space character in the input? What if the string has characters other than letters? And we can go on and on like this.
The code is written correctly, and the reader knows what it should do and what it is doing precisely. But what if it is not doing what it is supposed to, that is, what purpose should it serve? What if the explanation was:
“The function returns a positive result provided that the word input reads the same from the beginning as from the end. In all other cases (including erroneous inputs), the function returns a negative result.”
Such explanations are really far from perfect, but at least they respond to the problem of lowercase/uppercase letters (it should read the word; thus the capitalisation is irrelevant) and also to whether to throw an exception (the explanation clearly states that in such a case it should return a negative result). Here it can be argued that it is enough to edit the code and no documentation is necessary. But then we are going round in circles since the question arises as to why the application returns a negative value for erroneous inputs (errors with null are typically a sign of a programming error) and does not throw an exception.
A few tips for developers
The question of documentation detail often leads to philosophical debates that do not have a winner. So, the following three rules may be suitable for such discussions.
#1 Any public code can be someone’s future API
It is true! It does not matter if you are a team of one or one hundred. If anything in your code is available to others (typically with public visibility), then it is a potential candidate for an API and deserves documentation. The argument that you then have to maintain both the code and the documentation, thus introducing a potential source of errors, definitely does not stand. The programming documentation is part of the code—therefore it is either good or bad (hence the code revisions, etc.).
Think about any widespread, popular or widely recognised library (e.g. Spring/SpringBoot, Lombok, Material-UI, Bootstrap, etc.). Here it can be assumed that if something is so popular, and thus “good”, it is probably not written by below average/bad programmers (such as Rod Johnson). So, they could afford to write self-documenting code, but they don’t. Why not? Because the mark of quality, the mark of well-written code, is hidden even in effectively written and user-library supported documentation. Those who are aware of this pay due attention to the documentation. Or do you generally choose undocumented pieces of code instead of well-documented libraries with lots of illustrations and examples?
#2 Do you believe that reverse-engineering someone else’s code is faster than reading the documentation?
Let me give you an example directly from the JDK, the widely used method:
public String substring(int beginIndex, int endIndex)
What could be easier, right? However, the first time you use it, you will wonder whether the end index is exclusive or inclusive, which is quite crucial. In this case, the documentation clearly states: “Returns a new string that is a substring of this string. The substring begins at the specified beginIndex and extends to the character at index endIndex – 1. Thus the length of the substring is endIndex-beginIndex.”
It is absolutely clear. It is possible to argue about why the second parameter is not named something like “endIndexExclusive”, but the documentation provides illustrated examples of what the function returns for what kind of input. Now, imagine that you don’t have the documentation. Let’s use the source code:
public String substring(int beginIndex, int endIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } if (endIndex > value.length) { throw new StringIndexOutOfBoundsException(endIndex); } int subLen = endIndex - beginIndex; if (subLen < 0) { throw new StringIndexOutOfBoundsException(subLen); } return ((beginIndex == 0) && (endIndex == value.length)) ? this : new String(value, beginIndex, subLen); }
The code is readable. It is clear what it does. But is it clear that the end index is exclusive? Not 100%. To be sure, it is necessary to proceed to the internal constructor, which generally does not allow the string class to be used publicly. Isn’t the documentation faster?
Here, the rule should apply that if you want to use someone else’s code, you analyse its suitability for your use first without documentation (a function name, function signature, parameter names). If you are not sure, continue with the available documentation of the function (e.g., JavaDoc), and if that does not say anything, only then proceed to the code. Sometimes it is necessary to analyse the source code even when there is documentation, but, honestly, if you can read something in peace without having to examine the code, it is typically faster.
#3 Documentation is long term; your memory is not
Maybe you know everything about your program. Maybe your entire team does too. But our outputs typically spend exponentially more time in operation than in development. Embrace the reality of maintenance – even with agile development and long-term development, there will be parts that you will always be tweaking and other parts that will be updated once every few months. But will you still have the same people on your team? Will you remember everything? Do you really want to relay the whole history of the program to new employees, or would you rather be able to refer them to the documentation? And what if you go back to your code in six months or two years—are you sure you will recall everything?
No one is talking about dozens of pages of documentation. There are very many approaches to maintaining documentation efficiently. For example, so-called architecture decision records are perfect for documenting (constantly changing) architectures. Next year, this blog post will be 10 years old, and the topic has already been relevant for many years. The likes of Martin Fowler have even addressed it. From today’s perspective, it can be said that this is an old concept—however, the reality is that it is not being used by nearly as many teams as it should be. Why this is so is a mystery to me.
So, what is self-documenting code?
In some discussions and articles, you may learn that self-documenting code is a complete myth. However, in my opinion, this is just the opposite extreme that I cannot agree with. Well-written code, which is readable and clearly communicates its functionality and purpose, should be the goal of every developer. But an essential aspect of this point is that by itself it is not enough. There is a difference between a well-written and self-explanatory fragment of code and a library, application, system, platform, etc. Each level of abstraction has its demands, but I believe that for most of us, quality code should always be complemented by quality documentation.
Author: Michal Petřík
Head of Software Development