JavaScript
Article
By Jani Hartikainen

15 Ways to Write Self-documenting JavaScript

By Jani Hartikainen
Last chance to win! You'll get a... FREE 6-Month Subscription to SitePoint Premium Plus you'll go in the draw to WIN a new Macbook SitePoint 2017 Survey Yes, let's Do this It only takes 5 min

This article was peer reviewed by Tim Severien and Mark Brown. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

Isn’t it fun to find a comment in code that’s completely out of place and useless?

It’s an easy mistake to make: you change some code, and forget to remove or update the comment. A bad comment won’t break your code, but imagine what would happen when debugging. You read the comment. It says one thing, while the code does another. You’ll probably end up wasting time figuring it out, and in the worst case, it might even mislead you!

But writing code with zero comments isn’t an option. In my more than 15 years of programming experience, I’ve never seen a codebase where comments were completely unnecessary.

However, there are ways to reduce the need for comments. We can make use of certain coding techniques to clarify our code, simply by using the programming language’s features to our advantage.

Not only does this help make our code easier to understand, it can also help improve the design of the program overall!

This type of code is often called self documenting. Let me show you how you can take this approach to coding right now. While the examples I’ll present here are in JavaScript, you can apply most of the techniques in other languages as well.

Overview of Techniques

Some programmers include comments as part of self-documenting code. In this article, we’ll only focus on code. Comments are important, but they’re a large topic to be covered separately.

We can split the techniques for self-documenting code into three broad categories:

  • structural, where the structure of code or directories is used to clarify the purpose
  • naming related, such as function or variable naming
  • syntax related, where we make use of (or avoid using) features of the language to clarify code.

Many of these are simple on paper. The challenge comes from knowing when to use what technique. I’ll show you some practical examples as we tackle each one.

Structural

First, let’s look at the structural category. Structural changes refer to shifting code around for enhanced clarity.

Move code into a function

This is the same as the “extract function” refactoring — meaning that we take existing code and move it into a new function: we “extract” the code out into a new function.

For example, try to guess what the following line does:

var width = (value - 0.5) * 16;

Not very clear; a comment here could be quite useful. Or, we could extract a function to make it self documenting:

var width = emToPixels(value);

function emToPixels(ems) {
    return (ems - 0.5) * 16;
}

The only change was I moved the calculation into a function. The function’s name is descriptive of what it does, so the code no longer needs clarification. As an additional benefit, we now have a useful helper function that you can use elsewhere, so this method also helps reduce duplication.

Replace conditional expression with function

If clauses with multiple operands can often be hard to understand without a comment. We can apply a similar method as above to clarify them:

if(!el.offsetWidth || !el.offsetHeight) {
}

What is the purpose of the above condition?

function isVisible(el) {
    return el.offsetWidth && el.offsetHeight;
}

if(!isVisible(el)) {
}

Again, we moved the code into a function and the code is immediately much easier to understand.

Replace expression with variable

Replacing something with a variable is similar to moving code into a function, but instead of a function, we simply use a variable.

Let’s take a look at the example with if clauses again:

if(!el.offsetWidth || !el.offsetHeight) {
}

Instead of extracting a function, we can also clarify this by introducing a variable:

var isVisible = el.offsetWidth && el.offsetHeight;
if(!isVisible) {
}

This can be a better choice than extracting a function — for example, when the logic you want to clarify is very specific to a certain algorithm used only in one place.

The most common use for this method is mathematical expressions:

return a * b + (c / d);

We can clarify the above by splitting the calculation:

var multiplier = a * b;
var divisor = c / d;
return multiplier + divisor;

Because I’m terrible at math, imagine the above example has some meaningful algorithm. In any case, the point is that you can move complex expressions into variables that add meaning to otherwise hard-to-understand code.

Class and module interfaces

The interface — that is, the public methods and properties — of a class or module can act as documentation on its usage.

Let’s look at an example:

class Box {
    setState(state) {
        this.state = state;
    }

    getState() {
        return this.state;
    }
}

This class could contain some other code in it as well. I purposely kept the example simple, to illustrate how the public interface is documentation

Can you tell how this class should be used? Maybe with a little bit of work, but it isn’t very obvious.

Both of the functions have reasonable names: what they do is clear from their name. But despite this, it’s not very clear how you should be using them. Most likely you would need to read more code or the documentation for the class to figure it out.

What if we changed it to something like this:

class Box {
    open() {
        this.state = 'open';
    }

    close() {
        this.state = 'closed';
    }

    isOpen() {
        return this.state === 'open';
    }
}

Much easier to see the usage, don’t you think? Notice that we only changed the public interface; the internal representation is still the same with the this.state property.

Now you can tell at a glance how the Box class is used. This shows that even though the first version had good names in the functions, the complete package was still confusing, and how, with simple decisions like this, you can have a very big impact. You always have to think of the big picture.

Code grouping

Grouping different parts of code can also act as a form of documentation.

For example, you should always aim to declare your variables as close to where they are being used as possible, and try to group variable uses together.

This can be used to indicate a relationship between the different parts of the code, so that anyone changing it in the future has an easier time finding which parts they may also need to touch.

Consider the following example:

var foo = 1;

blah()
xyz();

bar(foo);
baz(1337);
quux(foo);

Can you see at a glance how many times foo was used? Compare it to this:

var foo = 1;
bar(foo);
quux(foo);

blah()
xyz();

baz(1337);

With all the uses of foo grouped together, we can easily see which parts of the code depend on it.

Use pure functions

Pure functions are much easier to understand than functions that rely on state.

What is a pure function? When calling a function with the same parameters, if it always produces the same output, it’s most likely a so-called “pure” function. This means the function should not have any side effects or rely on state — such as time, object properties, Ajax, etc.

These types of functions are easier to understand, as any values affecting their output are passed in explicitly. You won’t have to dig around to figure out where something comes from, or what affects the result, as it’s all in plain sight.

Another reason these types of functions make for more self-documenting code is you can trust their output. No matter what, the function will always return output only based on what parameters you give it. It also won’t affect anything external, so you can trust it won’t cause an unexpected side effect.

A good example of where this goes wrong is document.write(). Experienced JS developers know you shouldn’t use it, but many beginners stumble with it. Sometimes it works well — but other times, in certain circumstances, it can wipe the whole page clean. Talk about a side effect!

For a better overview of what a pure function is, see the article Functional Programming: Pure Functions.

Directory and file structure

When naming files or directories, follow the same naming convention as used in the project. If there’s no clear convention in the project, follow the standard for your language of choice.

For example, if you’re adding new UI-related code, find where similar functionality is in the project. If UI-related code is placed in src/ui/, you should do the same.

This makes it easier to find the code and shows its purpose, based on what you already know about the other pieces of code in the project. All UI code is in the same place, after all, so it must be UI related.

Naming

There’s a popular quote about the two hard things in computer science:

There are only two hard things in Computer Science: cache invalidation and naming things. — Phil Karlton

So let’s take a look at how we can use naming things to make our code self documenting.

Rename function

Function naming is often not too difficult, but there’s some simple rules that you can follow:

  • Avoid using vague words like “handle” or “manage”: handleLinks(), manageObjects(). What do either of these do?
  • Use active verbs: cutGrass(), sendFile() — functions that actively perform something.
  • Indicate return value: getMagicBullet(), readFile(). This is not something you can always do, but it’s helpful where it makes sense.
  • Languages with strong typing can use type signatures to help indicate return values as well.

Rename variable

With variables, here are two good rules of thumb:

  • Indicate units: if you have numeric parameters, you can include the expected unit. For example, widthPx instead of width to indicate the value is in pixels instead of some other unit.
  • Don’t use shortcuts: a or b are not acceptable names, except for counters in loops.

Follow established naming conventions

Try to follow the same naming conventions in your code. For example, if you have an object of a specific type, call it the same name:

var element = getElement();

Don’t suddenly decide to call it a node:

var node = getElement();

If you follow the same conventions as elsewhere in the codebase, anyone reading it can make safe assumptions about the meanings of things based on what it means elsewhere.

Use meaningful errors

Undefined is not an object!

Everyone’s favorite. Let’s not follow JavaScript’s example, and let’s make sure any errors our code throws have a meaningful message in them.

What makes an error message meaningful?

  • it should describe what the problem was
  • if possible, it should include any variable values or other data which caused the error
  • key point: the error should help us find out what went wrong — therefore acting as documentation on how the function should work.
--ADVERTISEMENT--

Syntax

Syntax-related methods for self-documenting code can be a little bit more language specific. For example, Ruby and Perl allow you to do all kinds of strange syntax tricks, which, in general, should be avoided.

Let’s take a look at a few that happen with JavaScript.

Don’t use syntax tricks

Don’t use strange tricks. Here’s a good way to confuse people:

imTricky && doMagic();

It’s equivalent to this much more sane looking code:

if(imTricky) {
    doMagic();
}

Always prefer the latter form. Syntax tricks are not going to do anyone any favors.

Use named constants, avoid magic values

If you have special values in your code — such as numbers or string values — consider using a constant instead. Even if it seems clear now, more often than not, when coming back to it in a month or two, nobody will have any idea why that particular number was put there.

const MEANING_OF_LIFE = 42;

(If you’re not using ES6, you can use a var and it’ll work equally well.)

Avoid boolean flags

Boolean flags can make for hard-to-understand code. Consider this:

myThing.setData({ x: 1 }, true);

What is the meaning of true? You have absolutely no idea, unless you dig into the source for setData() and find out.

Instead, you can add another function, or rename an existing function:

myThing.mergeData({ x: 1 });

Now you can immediately tell what’s going on.

Use language features to your advantage

We can even use some features of our chosen language to better communicate the intention behind some code.

A good example of this in JavaScript are the array iteration methods:

var ids = [];
for(var i = 0; i < things.length; i++) {
  ids.push(things[i].id);
}

The above code collects a list of IDs into a new array. However, in order to know that, we need to read the whole body of the loop. Compare it with using map():

var ids = things.map(function(thing) {
  return thing.id;
});

In this case, we immediately know that this produces a new array of something, because that’s the purpose of map(). This can be beneficial especially if you have more complicated looping logic. There’s a list of other iteration functions on MDN.

Another example with JavaScript is the const keyword.

Often, you declare variables where the value is supposed to never change. A very common example is when loading modules with CommonJS:

var async = require('async');

We can make the intention of never changing this even more clear:

const async = require('async');

As an added benefit, if someone ever accidentally tries to change this, we’ll now get an error.

Anti-patterns

With all these methods at your disposal, you can do a lot of good. However, there are some things you should be careful about …

Extracting for the sake of having short functions

Some people advocate the use of tiny tiny functions, and if you extract everything out, that’s what you can get. However, this can detrimentally affect how easy the code is to understand.

For example, imagine you’re debugging some code. You look in function a(). Then, you find it uses b(), which then uses c(). And so on.

While short functions can be great and easy to understand, if you’re only using the function in a single place, consider using the “replace expression with variable” method instead.

Don’t force things

As usual, there’s no absolute right way to do this. Therefore, if something doesn’t seem like it’s a good idea, don’t try to force it.

Conclusion

Making your code self documenting goes a long way to improving the maintainability of your code. Every comment is additional cruft that has to be maintained, so eliminating comments where possible is a good thing.

However, self-documenting code doesn’t replace documentation or comments. For example, code is limited in expressing intent, so you need to have good comments as well. API documentation is also very important for libraries, as having to read the code is not feasible unless your library is very small.

Login or Create Account to Comment
Login Create Account
Recommended
Sponsors
Get the most important and interesting stories in tech. Straight to your inbox, daily.
Is it good?Is it good?