This post previously appeared on SubMain’s blog.
You’re a clean coder. You use descriptive names for everything. You’ve refactored your app into a shrine of single responsibilities. Even your slightly crazy, off-the-grid uncle can follow the code. (Hi, Uncle Joe!) The app not only documents itself, it documents life.
Or does it?
Are there things you might have missed? Is there subtle context you’re going to forget between now and next week? And what crazy coding gems are you going to find a few months from now when you go to add a new feature?
Where does self-documenting code fall short?
Act I: The Failures of Comments
We’ve all been there. Either we were taught strict commenting rules or they were imposed on us.
Thou shall comment every single line!
So you did. And you looked at it and saw that it was bad.
Not only did you write the code, but you also rewrote the code as comments.
Your variables had comments. All your getters and setters had comments. And your for-loops brazenly announced to the world that they will “loop through all the elements!”
And where you actually needed context? There was none. You had been so pushed into mindlessly commenting that you never learned what a good comment could be.
Reading the comments of others proved also to be a depressing task. What do they mean? How does this make sense? Why don’t these comments match what the code is doing?!?!
There has to be a better way. Comments are not the answer.
Act II: The Self-Describing Promise
Good news! You’ve seen the light. There is obviously a problem, and you’re the one that is going to solve it.
No comments will be added by your educated self ever again.
So what are you doing right?
You’re following all the clean code principles. Not only are you naming methods and classes properly, but you’re making sure that anyone can come in and follow along. The names are meaningful and specific. And although naming things is hard, you’ve pledged to put thought into every decision.
You’re improving, and things are good.
Your code is easier to read. You remember what functions do for more than 20 minutes. The naming helps. The decomposition splits things properly. You feel like a craftsman coder.
The future feels promising. In three months when you come back to this code, maintenance will take no time at all.
This is easy!
But what if your code isn’t as easy to understand as you think it is?
Act III: The Betrayal
Things are humming along. You’re making good progress, and your code no longer stinks.
But every once in a while, you come across something that feels off. Sometimes things fall through the cracks. Parts of the code are not as clear as it seemed they were just a few months ago.
There are a bunch of different ways self-documenting code can betray you:
1. Something Is Stale
You run across a method. It’s called “addPreferredMemberDiscount.” But it doesn’t add the preferred member discount. It adds all types of discounts based on parameters. You check the commit logs. You find that it was, in fact, you that made the change.
But you never updated the name.
Wasn’t this only supposed to be an issue with comments?
2. You Can’t Find the Why
A few days later, you’re adding some new functionality for that change in shipping charges. You find some logic that is rounding up weight in some scenarios but rounding it down in others.
Unfortunately, you don’t remember why it’s acting this way. Is this important? Was this logic added because of a bug? You don’t know.
That’s OK, though. You’ll get this.
3. You’re Not Sure How You Got Here
Your last few fumbles were nothing. There are some new product recommendation algorithms that you are on top of. You’re going to kill it! OK, now how do these work? What are they doing?
No worries. Let’s go look at the unit tests. They will tell you more and they are for sure up to date.
You’re off to review the unit tests. So you pop open another file and review each of the five unit tests that are relevant. Well, that’s what you do once you find the tests that are relevant. Oh, and there are some weird abstractions and helper methods in these tests. So let’s go look at those, too.
Hmm…there seems to be some setup required. What does that look like?
OK. You’ve got this. It took some time, but you now know exactly what’s going on. Now, what is it that you came here to do? What were you actually hoping to learn? Do you remember why you’re even here?
4. Naming Things Is Hard
Looking back at code you wrote a few weeks ago, you find that the names are hard to understand. Not only do you have difficulty following the intent of names you wrote, you find it’s even harder with names your teammates decided were good ideas.
And then there are the method names that go on forever. Sure, they tell you everything you need to know. Maybe it’s just your dwindling attention span, but you’re starting to realize that reading “theBestFunctionNameForThisRandomThingThatOnlyOccursInThisScenario” really sucks. You skim it, skip it, and wind up overlooking that one key word that turns true into false. And all of a sudden you’re back at looking at implementation to figure out what’s going on.
The point is that you’re not as good at naming things as you think you are. That doesn’t mean that you should give up; you will get better. But it’s important to know your limitations and know when you need help.
5. Too Much Decomposition Gives You Dirt
Making sure your classes and functions are fine-grained is key. However, breaking things down too finely can result in lost context. It’s hard to see all at once. Although you want to see the big picture, you have to draw a map to get there.
This doesn’t mean that decomposition and single responsibilities are bad. On the contrary, they are necessary. But something’s missing.
Although everything is fine-grained and well defined, it’s difficult to see the whole picture. Looking at one piece of code requires that you dig through a lot of dirt before you can see how it all fits together.
Act IV: The Reunion
The good thing is that you’ve learned a lot! Clean code is important, and you can see your progress, which is remarkable. You can barely even recognize your old code.
But as we’ve seen, there are some hiccups. Now it’s time to find the middle ground.
There’s a difference between perfect and good enough. As long as you have named things as well as you can, you’re still allowed to write comments to fill in the blanks.
You know that your code is going to change. Do you really want to spend 20 minutes naming something that won’t make sense to you in three weeks? Or would you rather take 20 seconds to write a brief comment?
The goal is to speed up understanding. You’ve probably heard that we spend much more time reading old code than writing new code. So make it readable. Make it clean. And where you can’t, then add a comment.
Best Situations for Using Comments
So what are some things comments work well for?
- Explaining why something is done. Comments should never tell you what you’re doing, but instead what the intent of the code is.
- Demystifying magic numbers. The max payload size is 256k. I see that you named it well. But why is it that size?
- Describing why one solution was picked over another. Perhaps there are some performance limitations to the “obvious” solution. Let the reader know the reasoning behind the decision, but only if this reasoning adds to the reader’s overall understanding.
- Highlighting reasons for skipping conventions. Perhaps a library you are using has some unique requirements that force an unusual approach. Don’t make other developers or future-you have to go through the process of learning that lesson again.
- Commenting on the big picture and relationships that are hard to see. Give other developers an idea of how the pieces fit together.
Think about the last time you used a library. When you’re wondering about the use of a method or the reason for thrown exceptions, do you search for documentation? Or do you drill into the pertinent method and check the comments?
Even if you’re not writing a library, consider what other developers on your team need to know to make the right decisions. Consider what future-you needs to know. And remember that our natural language is often more suited for expressing and understanding ideas than code is.
As much as we try, code cannot communicate purpose. You have a choice to make: you can cargo cult your way to comment-less code, or you can do what makes sense.
Ultimately, comments are just tools that were designed to fix particular problems. Use them properly.