How to make your code self-documenting
When writing our apps, we often tend to overlook the simplest principle when it comes to coding: maintaining a clean and readable source code. You probably heard the phrase of spaghetti code before. Meaning the codebase is hard to maintain, read or even decipher. Even though the application could work just fine, adding new requirements and keeping support can be equal to hell. And if it comes to bugfixing, time spent searching through the code will grow exponentially.
So what is clean code anyway? The answer may differ from person to person but for me, clean code is self-documenting. The intent is clear from first sight, different responsibilities are well separated. You can almost read code like natural language and this means you don’t have to use comments as a crutch. Always favor code instead.
There are some common coding principles which can help us achieve that:
KISS means “keep it simple, stupid” and is originating from the U.S. Navy. The principle states that your code will work best if kept simple. Unnecessary complexity will only decrease readability and makes your code hard to take in. Therefore, you should always strive for simplicity. This doesn’t necessarily mean you should always use the shorter solution however. You may be able to write a nested if-else statement in one line, but the question is should you?
DRY means “don’t repeat yourself”. This may sound common sense but it’s not always so obvious. Take a look at the following code example:
Different things are happening for each call with different callbacks. Yet if you look at this from a distance, you will see repetition. The principle states that we should try to avoid redundancy. The above code can be simplified like this:
update function updates the
user object on the frontend, we can outsource that line into the function itself. Another example would be the use of variables that are likely to change like class names. Instead of using them scattered throughout your code, try to store them in one place so things can be easily reconfigured if needed.
TED means “terse, expressive and does one thing”. With TED, we are aiming to keep the signal to noise ratio high, where the signal is the code and the noise is anything that is trying to make our code messy. Just like duplications, extensive comments, high cyclomatic complexity or poor naming.
It is needed to allocate some time to take care of messes, otherwise it will continue to grow and will need to be addressed later down the road as tech debt. Because of increasing complexity, bugs may likely to be introduced.
Last but not least, the most commonly used principle, which is an acronym where each letter is a principle in itself:
(S)ingle responsibility principle
- It states that a class or function should only have a single responsibility. Same functions having different behaviours only increase complexity and decrease readability.
- Classes and functions should be open for extension but closed for modification. Already existing implementation should stay intact. Unless you are refactoring.
(L)iskov substitution principle
- It states that objects in a program should be replaceable with their instances without altering the behaviour of that program.
(I)nterface segregation principle
- This principle is fairly straightforward. It simply states that we should rather have many specific interfaces than one generic. Meaning you should not force methods to have functionality it does not use. Instead, impelement from multiple interfaces if needed.
(D)ependency inversion principle
- This principle is all about decoupling your code. It means that top-level modules should not depend on low-level ones. Or saying it another way; depend on abstractions not on concretions.
Following the principles listed above ensures that you one step closing achieving a code which is robust, reliable and easy to extend and understand.
#2: Naming Conventions
Next we have naming conventions. Names alone can decide how readable your code is. Let’s take the very first code example which was used in KISS and rename everything:
A lot of question arises. What each of them are doing? And what are they suppose to mean?
Always try to verbalize your code. Be specific and avoid generic prefixes as well as abbreviations. As we could see from the example above, there’s no clear intent on what a variable is suppose to hold. Some example are:
#3: The Use of Conditionals
When it comes to booleans, there are a couple of things we can take care of to make our code more expressive. And by doing so, we reduce the mental capacity it is needed to comprehend an implementation.
Also use databases instead of hardcoded long list of
switch cases for logics such as:
- pricing information
- complex and dynamic business rules
- data that often changes
This way you avoid hardcoding values, make things easier to update and you write less code which in the end means your code will be less error prone.
#4: The Use of Functions
There are a couple of things when it comes to functions. First, when should you create one? Only create functions when you see code duplication, when indenting becomes an issue or when intent is unclear.
Try to use expressive names, just as for variables or booleans.
Avoid using words in function names such as “and”, “or” as that is an indication the function has more responsibility than it needs to. Having single responsibility also makes it easier to understand the function, it promotes reusability and helps to avoid side effects. If your function is pure — meaning it returns the same value given the same input — it also eases testing.
When using arguments, try to aim for 0–2. If you need to use more than that, use an object instead of a list.
Also try to avoid using flags. It’s an indication that the function does more than one thing. And if you can, provide default values instead of using short circuits.
We talked about the outlines of a function, but what about the inner parts?
When writing a new function, return early and fail fast. This means having your return statements and exception handling at the top. Not only it will make your function more readable, it also improves its efficiency.
When using loops, prefer the function programming way such as
reduce over traditional for loops:
Also when you define variables inside the function, don’t declare them all on the top. Use them just when they are needed. They are called mayfly variables.
Lastly, when working with multiple async functions that depend on each other, instead of nesting, try to outsource the callback functions.
So how long your functions should be? If you need to scroll to see all of the function you need to reconsider your choices. According to Robert c. Martin, your functions should rarely be over 20 lines, hardly ever over 100 lines and they should take no more than 3 params. There will always be exceptions, but having too many lines of code for a single function may be the indicator that something is off.
#5: Don’t Overdo Comments, Use it Wisely
Using comments can be tempting when you are dealing with complex logic. But before trying to write an essay think about twice if you can express the same thing using only code. Only use comments when code alone is not sufficient. Don’t comment the obvious.
If you ever find yourself commenting because the intent is unclear, it is a sign that your code can be improved. Take the following as an example:
Try to avoid warnings. If you need to add a warning comment, it’s a sign that you need to refactor. Instead, fix it before you commit. Also, only add a
TODO marker if you must.
Don’t include commit hashes, pull request, ticket numbers or any metadata in the code that belongs to the source control. That is just additional noise which can be easily found without a comment.
Don’t ever leave commented out code, otherwise known as “zombie code” in your codebase. It is not used, therefore it serves no purpose. It reduces readability and only brings confusion:
- Why this was commented out? Was is on purpose or did someone do it accidentally?
- Do I need to refactor this? What if I uncomment it back? What did this section even do?
Since deleted code is visible in source control, you can always revert back changes if needed.
Another great example of comment which indicates that refactor is needed are dividers and brace trackers:
This tells us that the complexity is too high and the code may need to be broken down into pieces.
Use comments with caution, use them for summary and examples. Use them for documentation.
If you take these advices it will bring your read and maintainability to the next level. The next time you need to address an issue or implement a feature request, may your journey be fast and seamless.
As a last word, I would like to close this article with a great quote from Martin Fowler:
Any fool can write code that a computer can understand. Good programmers write code that humans can understand.