avatarLuís Soares

Summary

This context discusses the importance of self-documenting code and the problems associated with relying on code comments.

Abstract

The author argues that in most cases, if you rely on comments to explain code, you're doing it wrong. Comments can be subjective, allow bad code to exist, and quickly become outdated. The author suggests that whenever you need to comment, you should ask yourself if it can be converted to code. The ultimate goal should be to make code self-explanatory, which can be achieved by separating the intent from the implementation details and focusing on naming things. The author also suggests that code comments are not inherently bad, but rather the reliance on them. The article emphasizes the importance of making code self-explanatory and self-documenting.

Opinions

  • The author believes that relying on code comments can be a sign of bad code.
  • The author suggests that code should be self-explanatory and self-documenting.
  • The author emphasizes the importance of separating the intent from the implementation details.
  • The author believes that naming things properly is crucial in making code self-explanatory.
  • The author suggests that code comments are not inherently bad, but rather the reliance on them.
  • The author emphasizes the importance of making code self-explanatory and self-documenting.

Towards self-documenting code — part I

This is not a case against code comments; it’s about favoring a self-explanatory and self-evident codebase. Comments become superfluous as a side-effect.

In most cases, if you rely on comments to explain code, you’re doing it wrong. I’ll show you some ways to achieve self-documenting code. No, it’s not utopic.

Photo by Artur Shamsutdinov on Unsplash

No comments by default

In my case, the “code-needs-comments” myth comes from the university. However, the rule to make your code self-explanatory rather than using comments existed in all the companies I was at, and it worked out fine. So, what are the problems with code comments?

Subjectivity. Comments are subject to the reader’s interpretation. Natural languages are ambiguous, but code doesn’t lie.

Code never lies; comments sometimes do. (Ron Jeffries)

They allow bad code to exist. Comments are an excuse to make bad code with good explanations.

A comment is a failure to express yourself in code. If you fail, then write a comment; but try not to fail. (Robert Martin)

They quickly get outdated. The code is the ultimate source of truth, so why have another? Code changes while comments can go stale and outdated comments confuse more than they help. Most coders have comment blindness, so believing others will read and update them is wishful thinking. A comment is a lie waiting to happen.

The comments will inevitably become out of date, and untrustworthy comments are worse than no comments at all. The Pragmatic Programmer

Don’t make me think

Comments are not inherently bad, but rather the reliance on them. Whenever you need to comment, ask yourself: “Can it be converted to code? If not, why not?”. Try acknowledging the underlying smells rather than adding comments, or you may just be adding band-aids.

A comment is the code’s way of asking to be more clear. (Kent Beck)

The saying goes, “First make it work, then make it right, and, finally, make it fast”. Doing it right mostly means making it self-explanatory. If you have to explain code, you’re doing it wrong. Self-documenting code is self-explanatory and screams its intent. This is precisely for the same reason that a good UI should not rely on instructions. Communicating using code is widely advocated by the XP community.

Your objective should always be to eliminate instructions entirely by making everything self-explanatory, or as close to it as possible. Don’t Make Me Think

Many teachings from the book Don’t Make Me Think could also be applied to codebases.

Rule of Least Surprise: In interface design, always do the least surprising thing. (This is also widely known as the Principle of Least Astonishment.) The Art of UNIX Programming

The common objection to avoiding code comments is that they serve as documentation of non-trivial code. So, why not make it trivial? Don’t stick to the first implementation you came up with. Usually, the ideal solution is the simplest one. The paradox is that there’s no quick way to get it simple. Therefore, iterate multiple times. Simplicity will naturally emerge from the iterations. This is related to human psychology. If you tell yourself, “It’s fine; I’ll explain it with comments”, it’s less likely you won’t make those iterations.

Don’t comment bad code — rewrite it. The Elements of Programming Style

When you feel the need to write a comment, first try to refactor the code so that any comment becomes superfluous. Refactoring

Whenever you feel like writing a comment, ask if you could express it using variables, functions, classes, and other programming elements. To achieve it, clarify the intent by separating the what (intent/purpose) from the how (implementation details). If I could say only one thing, it would be to focus on intent, not technical details. Let’s see examples of that mindset.

Naming things

The ubiquitous language, a term from domain-driven design, is the agreed-upon language between the team and the users. Codebases should reflect this in every aspect, including names of files, folders, classes, functions, and variables. Refactoring should be done to reflect changes in the ubiquitous language.

To communicate effectively, the code must be based on the same language used to write the requirements — the same language that the developers speak with each other and with domain experts. Domain-Driven Design

A lot could be said about naming variables, but the most critical rule is to focus on the purpose, not the technicalities. Use names that describe what is happening in terms of the bigger picture, not how it is done in detail. Leave the details to the implementation of the thing named. Don’t encode the types in the variables’ names because that highlights a technical detail. Use business-sounding names (not verbs) instead as that explains the why, it helps to “tell the story”.

// 🔴 highlights technical type (Hungarian notation)
val clientList = listOf(x, y, z)
val callback = { x, acc -> x + acc }
val string = "[email protected]"

// 🟡 hides technical type
val clients = getClients()
val calculate = { x: Int -> x * 0.7 }
val email = "[email protected]"

// 🟢 solely about the business intent
val eligibleClients = listOf(x, y, z)
val applyDiscount = { x: Int -> x * 0.7 }
val notificationAddress = "[email protected]"

The same reasoning applies to functions, methods, and use cases. Make their purpose evident by starting with a verb, focusing on the intent, and relying on business terminology.

We have enough encodings to deal with without adding more to our burden. Encoding type or scope information into names simply adds an extra burden of deciphering. […] It is an unnecessary mental burden when trying to solve a problem. Encoded names are seldom pronounceable and are easy to mis-type. Clean Code

📝 If you’re having difficulty naming a class, check if it’s not hiding any smell. The class may lack cohesion — i.e., it might be doing unrelated things or too much. Class names like hub, manager or service are black holes of code.

The length of the variable name also conveys meaning: short names like “i” have a lower scope than bigger names like updatedCustomers.

Don’t mix levels of abstraction

Most code comments within functions could be turned into private helper functions. Compare the following:

# parse request
code…
# delete user from server
code…
# send email to the user
code…

🆚

user = parse(request)
delete(user)
notify(user)

The first approach is full of technical details and resorts to comments to explain what is going on, while the second approach relies on the code itself to do it; it separates the whats from the hows.

If you have to spend effort into looking at a fragment of code to figure out what it’s doing, then you should extract it into a function and name the function after that “what”. That way when you read it again, the purpose of the function leaps right out at you, and most of the time you won’t need to care about how the function fulfills its purpose — which is the body of the function. Function Length

This is known as the principle of the single level of abstraction, which tells us not to mix different levels of abstraction in the same function. Delegating details to private functions creates a high-level language that is closer to an actual spoken language. You can vertically scan the code reading “do this, then do that, finally that…” without having to grasp the implementation details simultaneously. Additionally, making each function/class do one thing well can greatly help readability.

In order to make sure our functions are doing “one thing,” we need to make sure that the statements within our function are all at the same level of abstraction. Clean Code

Switching between levels of abstraction makes code harder to read. While reading the code you have to mentally construct the missing abstractions by trying to find groups of statements which belong together. Single Level of Abstraction (SLA)

📝 Don’t refrain from a long function name if you need to express what it does (e.g., hackForCompatibilityWithInternetExplorer).

Make your app’s intentions clear

The screaming architecture states the app should be oriented to its business/user intents rather than technical details like framework artifacts. It recommends that your architecture has a clear set of use cases. Use cases are the embodiment of the software functionalities.

Here’s what an application should look like. The use cases should be the highest level and most visible architectural entities. The use cases are at the center. Always! Databases and frameworks are details! NO DB

I make the app’s use cases clear by having one per file — this allows a glance to know what the app does, thereby being a source of self-documentation. I apply this guideline in the implementation (entry points and the domain use cases) and the tests. Also, I put the related errors/exceptions and request/response DTOs together with the use case since colocation provides context.

A folder structure telling about technology vs. one about intents

I avoid having long files of business code like hubs or services. A hotspot is like a black hole of code that you must work with often. There’s always a way to split it.

Just as the plans for a house or a library scream about the use cases of those buildings, so should the architecture of a software application scream about the use cases of the application. Screaming Architecture

Centralize the wiring of your application

The way you set up your app’s dependency injection is also a great source of documentation. It provides a high-level view of the technical and business side of the app. Besides, it tells what depends on what. For example, you can quickly see the external parties that your app integrates with. Here’s an example:

val app = RecordingsApp(
  accessControl = AccessControl(),   
  recordingRepo = RecordingRepo(
      database = Database.connect(
         url = System.getenv("DB_URL")
      ),
      logger = MyLogger(
         level = Level.WARN,
      ),
  ),
  recordingUploader = RecordingUploader(
     apiBaseUrl = System.getenv("API_URL"),
  ),
  clock = Clock.systemUTC(),
  monitor = NewRelicMonitor(),
)

Part II

Code Comments
Clean Code
Good Practices
Self Documenting Code
Self Explanatory
Recommended from ReadMedium