When Comments Should Explain âWhatâ and When They Should Explain âWhyâ
Choosing between explaining the *what* or the *why* of code in comments is a nuanced decision. While descriptive code should carry the meaning, some scenarios still warrant a brief explanation to avoid context switches and misunderstandings. This article explores the tradeâoffs, illustrates with concrete examples, and offers practical guidelines for engineers.
### The LongâStanding Rule of Comments
Many developers follow the adage that comments should explain *why* something is done, not *what* it is doing. The logic is simple: the code itself, using clear names and structure, should reveal the *what*. If you find yourself writing a comment that merely repeats the code, the code is likely unclear.
However, in practice this principle can clash with realâworld constraints. A comment that points out why a line is repeated, or why a particular implementation choice has been made, provides value that a refactor alone might not capture. The key is to balance clarity with maintainability.
### A Case Study: Variable Naming vs. Commenting
Consider the following snippet:
```java
// weight, radius, price
w = 10, r = 9, p = 1
```
Here a terse comment attempts to explain each variable. Without the comment, the reader would have to infer that `w` is *weight*, `r` is *radius*, and `p` is *price*. While the variables are short, the comment forces the reader to **scroll back and forth** every time they encounter `w` later in the code. This context switch can be especially disruptive when the variables are used in a complex algorithm.
Contrast this with a selfâdocumenting alternative:
```java
weight = 10;
radius = 9;
price = 3;
```
By using explicit names, the code is immediately readable, and no comment is necessary. The lesson? Prefer expressive identifiers over *what* comments whenever possible.
### Why Some People Prefer Commit Messages
A frequent argument against *why* comments is that they should live in commit messages or tests, keeping the source code clean. Imagine this line in production code:
```java
// Clear twice to deal with bug ABC in library XYZ, see [link]
XYZ.clear();
XYZ.clear();
```
If the comment is removed and the explanation documented in the commit, any developer reading the code in the future has to hunt down the relevant commit. That requires a context switch and an extra mental cost. Moreover, if the line is refactored or reordered, `git blame` may no longer point to the original commit, breaking the link between code and rationale.
For a team that values **immediate readability**, embedding a concise rationale in the code itself can save countless hours of digging.
### Refactoring vs. Commenting: The BobâŻMartin Example
BobâŻMartinâs *ExtractâTillâYouâDrop* technique encourages breaking complex logic into tiny methods. The refactored version of a symbol replacement routine looks like this:
```java
String replace() {
replaceAllSymbols();
return stringToReplace;
}
private void replaceAllSymbols() {
for (String symbolName = nextSymbol(); symbolName != null; symbolName = nextSymbol())
replaceAllInstances(symbolName);
}
private String nextSymbol() { /* ⊠*/ }
private void replaceAllInstances(String symbolName) { /* ⊠*/ }
private boolean shouldReplaceSymbol(String symbolName) { /* ⊠*/ }
private void replaceSymbol(String symbolName) { /* ⊠*/ }
private String symbolExpression(String symbolName) { /* ⊠*/ }
```
While the refactor pulls the bulk of the logic into separate methods, a developer now must **jump across method boundaries** to grasp the flow. Conversely, keeping the logic in a single method and adding explanatory comments can preserve the *inâplace* understanding.
For example, the original singleâmethod implementation with informative comments is easier to read for someone tracking a bug:
```java
String replace() {
Pattern symbolPattern = Pattern.compile("\\$([a-zA-Z]\\w*)"); // e.g. $F1a3
Matcher symbolMatcher = symbolPattern.matcher(stringToReplace);
// Replace all symbols
while (symbolMatcher.find()) {
String symbolName = symbolMatcher.group(1);
// translate replaces all instances; run once per symbol
if (getSymbol(symbolName) != null && !alreadyReplaced.contains(symbolName)) {
alreadyReplaced.add(symbolName);
stringToReplace = stringToReplace.replace("$" + symbolName, translate(symbolName));
}
}
return stringToReplace;
}
```
In this scenario, the *why* comment clarifies the intent of `translate` and the reasoning for the guard clause. The benefit outweighs the tiny increase in lines.
### Practical Guidelines
1. **Prioritize descriptive names**. If a variable or method can be named to reveal its purpose, do so. Avoid *what* comments.
2. **Use *why* comments sparingly but where they reduce cognitive load**. When an idiom, workaround, or historical quirk is necessary, document it.
3. **Keep related comments near the code**. Whether the comment explains *what* or *why*, proximity matters. Donât hide explanations in commit logs if the code may be read independently.
4. **Avoid redundant comments**. A comment that simply repeats the code is a maintenance burden.
5. **Leverage tests and documentation for additional rationale**. Unit tests can validate the *why*âfor example, that a certain edge case is handled.
6. **Maintain consistency across the codebase**. Agree on guidelines with your team and enforce them in code reviews.
### Conclusion
The dichotomy of **what** vs **why** comments is not absolute. Good code often conveys the *what*; additional *why* comments are justified when they reduce cognitive friction and help future maintainers. Striking the right balanceâusing expressive code as the first line of defense and augmenting it with succinct comments for complex or nonâintuitive decisionsâleads to more robust, selfâdocumenting systems.