The Importance of Writing Code That Humans Can Read
The Importance of Writing Code That Humans Can Read
This article was peer reviewed by Matt Burnett, Simon Codrington and Nilson Jacques. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!
Have you ever finished a project in a single run without ever needing to look at the code again? Neither have I. When working on an older project, you probably want to spend little to no time figuring out how code works. Readable code is imperative to keep a product maintainable and to keep yourself, and your colleagues or collaborators happy.
Exaggerated examples of unreadable code can be found on JS1k contests, where the goal is to write the best JavaScript applications with 1024 characters or less, and JSF*ck (NSFW, by the way), an esoteric programming style which uses only six different characters to write JavaScript code. Looking at code on either of these sites will make you wonder what is going on. Imagine writing such code and trying to fix a bug months later.
If you surf the internet regularly, or build interfaces, you may know that it’s easier to quit a large, bulky form than one that seems simple and small. The same can be said about code. When perceived as easier to read and to work on, one may enjoy working on it more. At least it will save you tossing out your computer in frustration.
In this article, I’m going to look at tips and tricks to make your code more readable, as well as pitfalls to avoid.
Code Splitting
More from this author
Sticking with the form analogy, forms are sometimes split in parts, making them appear less of a hurdle. The same can be done with code. By splitting it up into parts, readers can skip to what is relevant for them instead of ploughing through a jungle.
Across Files
For years, we have been optimising things for the web. JavaScript files are no exception of that. Think of minification and pre-HTTP/2, we saved HTTP requests by combining scripts into a single one. Today, we can work as we want and have a task runner like Gulp or Grunt process our files. It’s safe to say we get to program the way we like, and leave optimization (such as concatenation) to tools.
// Load user data from API
getUsersRequest XMLHttpRequest
getUsersRequest'GET' '/api/users'
getUsersRequestaddEventListener'load' function
// Do something with users
getUsersRequest
//---------------------------------------------------
// Different functionality starts here. Perhaps
// this is an opportunity to split into files.
//---------------------------------------------------
// Load post data from API
getPostsRequest XMLHttpRequest
getPostsRequest'GET' '/api/posts'
getPostsRequestaddEventListener'load' function
// Do something with posts
getPostsRequest
Functions
Functions allow us to create blocks of code we can reuse. Normally, a function’s content is indented, making it easy to see where a function starts and ends. A good habit is to keep functions tiny—10 lines or less. When a function is named correctly, it’s also easy to understand what is happening when it’s being called. We’ll get to naming conventions later.
// Load user data from API
function getUserscallback
getUsersRequest XMLHttpRequest
getUsersRequest'GET' '/api/users'
getUsersRequestaddEventListener'load' function
callbackJSONparsegetUsersRequestresponseText
getUsersRequest
// Load post data from API
function getPostscallback
getPostsRequest XMLHttpRequest
getPostsRequest'GET' '/api/posts'
getPostsRequestaddEventListener'load' function
callbackJSONparsegetPostsRequestresponseText
getPostsRequest
// Because of proper naming, it’s easy to understand this code
// without reading the actual functions
getUsersfunctionusers
// Do something with users
getPostsfunctionposts
// Do something with posts
We can simplify the above code. Note how both functions are almost identical? We can apply the Don’t Repeat Yourself (DRY) principle. This prevents clutter.
function fetchJsonurl callback
request XMLHttpRequest
request'GET' url
requestaddEventListener'load' function
callbackJSONparserequestresponseText
request
// The below code is still easy to understand
// without reading the above function
fetchJson'/api/users' functionusers
// Do something with users
fetchJson'/api/posts' functionposts
// Do something with posts
What if we want to create a new user through a POST request? At this point, one option is to add optional arguments to the function, introducing new logic to the function, making it too complex for one function. Another option is to create a new function specifically for POST requests, which would result in duplicate code.
We can get the best of both with object-oriented programming, allowing us to create a configurable single-use object, while keeping it maintainable.
Note: if you need a primer specifically on object-oriented JavaScript, I recommend this video: The Definitive Guide to Object-Oriented JavaScript
Object Oriented Programming
Consider objects, often called classes, a cluster of functions that are context-aware. An object fits beautifully in a dedicated file. In our case, we can build a basic wrapper for XMLHttpRequest.
HttpRequest.js
function HttpRequesturl
request XMLHttpRequest
body undefined
method HttpRequestMETHOD_GET
url url
responseParser undefined
HttpRequestMETHOD_GET 'GET'
HttpRequestMETHOD_POST 'POST'
HttpRequestprototypesetMethod functionmethod
method method
return
HttpRequestprototypesetBody functionbody
typeof body 'object'
body JSONstringifybody
body body
return
HttpRequestprototypesetResponseParser functionresponseParser
typeof responseParser 'function' return
responseParser responseParser
return
HttpRequestprototypesend functioncallback
requestaddEventListener'load' function
responseParser
callbackresponseParserrequestresponseText
callbackrequestresponseText
false
requestmethod url
requestbody
return
app.js
HttpRequest'/users'
setResponseParserJSONparse
functionusers
// Do something with users
HttpRequest'/posts'
setResponseParserJSONparse
functionposts
// Do something with posts
// Create a new user
HttpRequest'/user'
setMethodHttpRequestMETHOD_POST
setBody
name 'Tim'
email 'info@example.com'
setResponseParserJSONparse
functionuser
// Do something with new user
The HttpRequest class created above is now very configurable, so can be applied for many of our API calls. Despite the implementation—a series of chained method calls—being more complex, the class’s features are easy to maintain. Finding a balance between implementation and reusability can be difficult and is project-specific.
When using OOP, design patterns make a great addition. Although they don’t improve readability per se, consistency does!
Human Syntax
Files, functions, objects, those are just the rough lines. They make your code easy to scan. Making code easy to read is a much more nuanced art. The tiniest detail can make a major difference. Limiting your line length to 80 characters, for example, is a simple solution that is often enforced by editors through a vertical line. But there’s more!
Naming
Appropriate naming can cause instant recognition, saving you the need to look up what a value is or what a function does.
Functions are usually in camel case. Starting them with a verb, followed by a subject often helps.
function getApiUrl /* ... */
function setRequestMethod /* ... */
function findItemsByIdn /* ... */
function hideSearchForm /* ... */
For variable names, try to apply the inverted pyramid methodology. The subject comes first, properties come later.
element documentgetElementById'body'
elementChildren elementchildren
elementChildrenCount elementChildrenlength
// When defining a set of colours, I prefix the variable with “color”
colorBackground 0xFAFAFA
colorPrimary 0x663399
// When defining a set of background properties, I use background as base
backgroundColor 0xFAFAFA
backgroundImages 'foo.png' 'bar.png'
// Context can make all the difference
headerBackgroundColor 0xFAFAFA
headerTextColor 0x663399
It’s also important being able to tell the difference between regular variables, and special ones. The name of constants, for example, are often written in uppercase and with underscores.
URI_ROOT windowlocationhref
Classes are usually in camel case, starting with an uppercase letter.
function FooObject
// ...
A small detail is abbreviations. Some chose to write abbreviations in full uppercase while others choose to stick with camel case. Using the former may make it more difficult to recognize subsequent abbreviations.
Compactness and Optimisation
In many codebases, you may come across “special” code to reduce the number of characters, or to increase an algorithm’s performance.
A one-liner is an example of compact code. Unfortunately, they often rely on hacks or obscure syntax. A nested ternary operator, as seen below, is a common case. Despite being compact, it can also take a second or two to understand what it does, as opposed to regular if-statements. Be careful with syntactical shortcuts.
// Yay, someone managed to make this a one-liner!
state isHidden 'hidden' isAnimating 'animating'
// Yay, someone managed to make this readable!
state
isAnimating state 'animating'
isHidden state 'hidden'
Micro-optimisations are performance optimisations, often of little impact. Most of the time, they are less readable than a less performant equivalent.
// This may be most performant
$elchecked
// But these are still fast, and are much easier to read
// Source: http://jsperf.com/prop-vs-ischecked/5
$el'checked'
$el':checked'
$el'checked'
JavaScript compilers are really good in optimising code for us, and they keep getting better. Unless the difference between unoptimised and optimised code is noticeable, which often is after thousands or millions of operations, going for the easier read is recommended.
Non-Code
Call it irony, but a better way to keep code readable is to add syntax that isn’t executed. Let’s call it non-code.
Whitespace
I’m pretty sure every developer has had another developer supply, or has inspected a site’s minified code—code where most whitespace is removed. Coming across that the first time can be quite a surprise. In different visual artistic fields, like design and typography, void space is as important as fill. You will want to find the delicate balance between the two. Opinions on that balance vary per company, per team, per developer. Luckily, there are some universally agreed rules:
- one expression per line,
- indent the contents of a block,
- an extra break can be used to separate sections of code.
Any other rule should be discussed with whoever you work with. Whatever code style you agree on, consistency is key.
function sendPostRequesturl data cb
// A few assignments grouped together and neatly indented
requestMethod 'POST'
requestHeaders
'Content-Type' 'text/plain'
// XMLHttpRequest initialisation, configuration and submission
request XMLHttpRequest
requestaddEventListener'load' cb false
requestrequestMethod url false
requestdata
Comments
Much like whitespace, comments can be a great way to give your code some air, but also allows you to add details to code. Be sure to add comments to show:
- explanation and argumentation of non-obvious code,
-
which bug or oddity a fix resolves, and sources when available.
// Sum values for the graph’s range
sum valuesreducefunctionpreviousValue currentValue
return previousValue currentValue
Not all fixes are obvious. Putting additional information can clarify a lot:
'addEventListener' element
elementaddEventListener'click' myFunc
// IE8 and lower do not support .addEventListener,
// so .attachEvent should be used instead
// http://caniuse.com/#search=addEventListener
// https://msdn.microsoft.com/en-us/library/ms536343%28VS.85%29.aspx
elementattachEvent'click' myFunc
Inline Documentation
When writing object-oriented software, inline docs can, much like regular comments, give some breathing space to your code. They also help clarify the purpose and details of a property or method. Many IDEs use them for hints, and generated documentation tools use them too! Whatever the reason is, writing docs is an excellent practise.
/**
* Create a HTTP request
* @constructor
* @param {string} url
*/
function HttpRequesturl
// ...
/**
* Set an object of headers
* @param {Object} headers
* @return {HttpRequest}
*/
HttpRequestprototypesetHeaders functionheaders
header headers
headersheader headersheader
// Return self for chaining
return
Callback Puzzles
Events and asynchronous calls are great JavaScript features, but it often makes code harder to read.
Async calls are often provided with callbacks. Sometimes, you want to run them in sequence, or wait for all of them to be ready.
function doRequesturl success error /* ... */
doRequest'https://example.com/api/users' functionusers
doRequest'https://example.com/api/posts' functionposts
// Do something with users and posts
functionerror
// /api/posts went wrong
functionerror
// /api/users went wrong
The Promise object was introduced in ES2015 (also known as ES6) to solve both issues. It allows you to flatten down nested async requests.
function doRequesturl
return Promisefunctionresolve reject
// Initialise request
// Call resolve(response) on success
// Call reject(error) on error
// Request users first
doRequest'https://example.com/api/users'
// .then() is executed when they all executed successfully
functionusers /* ... */
// .catch() is executed when any of the promises fired the reject() function
catchfunctionerror /* ... */
// Run multiple promises parallel
Promise
doRequest'https://example.com/api/users'
doRequest'https://example.com/api/posts'
functionresponses /* ... */
catchfunctionerror /* ... */
Although we introduced additional code, this is easier to interpret correctly. You can read more about Promises here: JavaScript Goes Asynchronous (and It’s Awesome)
ES6/ES2015
If you are aware of the ES2015 spec, you may have noticed that all code examples in this article are of older versions (with the exception of the Promise object). Despite ES6 giving us great features, there are some concerns in terms of readability.
The fat arrow syntax defines a function that inherits the value of from its parent scope. At least, that is why it was designed. It is tempting to use it to define regular functions as well.
add a b a b
console
Another example is the rest and spread syntax.
/**
* Sums a list of numbers
* @param {Array} numbers
* @return {Number}
*/
function numbers
return nreducefunctionpreviousValue currentValue
return previousValue currentValue
/**
* Sums a, b and c
* @param {Number} a
* @param {Number} b
* @param {Number} c
* @return {Number}
*/
function a b c
return a b c
My point is that the ES2015 spec introduces a lot useful, but obscure, sometimes confusing syntax that lends itself to being abused for one-liners. I don’t want to discourage using these features. I want to encourage caution using them.
Conclusion
Keeping your code readable and maintainable is something to keep in mind at every stage of your project. From the file system to tiny syntactic choices, everything matters. Especially on teams, it’s hard to enforce all rules all the time. Code review can help, but still leaves room for human error. Luckily, there are tools to help you with that!
- JSHint – a JavaScript linter to keep code error-free
- Idiomatic – a popular code style standard, but feel free to deviate
- EditorConfig – defining cross-editor code styles
Other than code quality and style tools, there are also tools that makes any code easier to read. Try different syntax highlight themes, or try a minimap to see a top-down overview of your script (Atom, Brackets).
What are your thoughts on writing readable and maintainable code? I’d love to hear them in the comments below.