This article is also available in Chinese.

When writing code, the biggest enemy is complexity. Maintaining levels of abstraction and allowing the developers who work in our codebases to fluidly move up and down through those concepts is crucial to large software projects.

Comments are a tool that have a veneer of managing complexity, but subtly hurt the codebase instead of helping.

My perspective on comments comes from two facts: 1) Comments don’t compile. 2) Comments are usually a subdued color in your syntax highlighter. Because they don’t compile and because they fade away from sight, it’s very easy to ignore them as you make changes to the code. If you make changes and don’t update the comments, you end up with a comment that doesn’t accurately reflect the content of code.

In an ideal world, you’d be able to catch this in code review, but I’ve never seen that work in practice. Code review tools have syntax highlighting as well, causing comments to fade into the background in that context as well. Also, because code review diffs only show a few lines around the change, out-of-date comments can only be found if the comment is close to the change itself. If you change a line deep in a method that changes what the preconditions of the method are, your reviewers won’t see the comment and won’t be able to tell you to update it.

There are a few tips and tricks to avoid the most common comments, some of which are covered in a blog post by an old manager of mine, and others of which Andrew can’t even dream of because he writes code in Ruby, a dynamically typed language.

  1. Name things well! The first step to avoiding comments is to avoid things like single-letter names, abstract names, and vague names. The more precise you can be with the name, the less you’ll need a comment.

  2. If a method has preconditions, add an assertion that will crash the app (at least in debug, if not always!) if an invalid value is passed in. If you only accept positive, non-zero integers, write a bit of code that says so: precondition(int > 0).

  3. Even better than run-time assertions is compile-time ones. If your method accepts only non-empty arrays, you could write precondition(!array.isEmpty). But you could also use a type that can never express an empty array. Users of your API will now never be able to pass an empty array into that parameter.

    In the same vein, do you have a bool parameter that’s better expressed via an enum with two named cases? Are your optionals better expressed as other enums? Reveal your intentions in your naming.

  4. Mark hacks, temporary code, and prototype code liberally. I often use a prefix like hack_ to mark that a function is not written ideally. An underscore in a method name in Swift looks so out of place that I directly feel the pain of making my codebase worse, and I’m inspired to try and fix it. We recently made a function prefixed with shouldReallyBeInTheCoordinator_ because something needed to get through code review, but the code wasn’t in the right class. The needs of your codebase and your own sensibilities become aligned when bad code is made to be ugly code as well. Other good prefixes: perf_ and temp_.

  5. Via Mark Sands: you can encode the IDs from your bug tracker into method names and they’ll show up in stack traces. UIKit references radar numbers in a few cases. This is real:

     -[UIViewController _hackFor11408026_beginAppearanceTransition:animated:]
    
  6. Don’t be afraid to describe the “why” of a function in its name. You’re allowed to make methods called updateFrameOnNextTickBecauseAutoLayoutHasntCompletedYet(frame: CGRect). The compiler doesn’t care how long your methods are named, and code is read much more than its written. Comments are just words, and so are method names. Future maintainers of that codebase will appreciate your verbosity.

  7. Make helper functions, like a function called TODO(date: Date, message: String) that logs an error (or even better, crashes in debug) if the TODO isn’t fixed by some date. Another example via Jordan Rose.

  8. Encode any algorithmic requirements into tests. If all of the above fails, and you can’t rely on the preconditions, types, and method names to solve a particular problem, write a test for it. This is especially good for edge cases. If someone rewrites that code, the tests will break, and they’ll know they have to handle that case with their new code.

Remember: this isn’t an excuse to write impenetrable code and not comment it! The code must be clear if you’re going to skip commenting it. Comments for me are a last resort. If I can find of any other way to express my intentions to the next programmer, I don’t add a comment.

Further Reading