This is the first part of a series, because otherwise it would be a 30m read. No one wants a 30m read. This isn’t a Stephen King novel!
I am known for writing clean, concise, human-readable JavaScript code and I have showcased this ability extensively on YouTube.
Now I will extract the rules I live by when at my keyboard, into a simple step-by-step algorithm so that you can use it to do the same.
By the end of reading this article, you should have the rules and tools you need to write the kind of code you “Uncle Bob” can be proud of!
I will be writing my examples in JavaScript, and certainly, some of these will be JavaScript (or TypeScript) centric, but almost all of it can be applied to any high-level programming language.
Most of my rules-of-the-road here were culled from much more experienced programmers I have interacted with over the years. Once you get your name at the top of an organization-wide email from the likes of someone like Doug Crockford. It just isn’t something you easily forget.
I will list a few of the people who most affected my coding style here, instead of trying to remember who said or wrote what:
If you are writing a “game loop” and need a high framerate, then by all means, write an imperative for-loop construct. However, if you are trying to write concise, decoupled, beautiful code that is less prone to bugs; use map
, filter
, and reduce
.
The map
function decouples the looping logic out of the “work to be done”. This has a couple of benefits, including not pushing code that spins out of control due to a missed ++ somewhere.
It also has a beneficial side effect - you don’t have to remember if you are counting up or down, and you don’t need to worry about if it is ++i or i++. That part of the looping mechanism is abstracted away from you.
Instead, you can focus on the “work to be done” - the part you care about writing. And the code you write will be shorter and easier to follow. You will also have quite a few fewer assignments changing values that you now have to keep track of.
Map
is the tool to reach for when you need to transform one array into another array of the same length as the original.
Imperative loop
const articles = [
{ title: "This is article title 1" },
{ title: "This is article title 2" },
{ title: "This is article title 3" }
];
const transformedArticles = [];
for (let i = 0; i < articles.length; i++) {
const article = articles[i];
transformedArticles.push({ ...article, slug: article
.title.toLowerCase()
.replace(/[^\w\s]/gi, "")
.trim()
.replace(/\s+/g, "-") });
}
Result
[
{
title: "This is article title 1",
slug: "this-is-article-title-1"
},
...
]
Functional fix
const articles = [
{ title: "This is article title 1" },
{ title: "This is article title 2" },
{ title: "This is article title 3" }
];
articles.map(article => ({
...article,
slug: slug(article.text)
}))
function slug(text) {
return text.toLowerCase()
.replace(/[^\w\s]/gi, "")
.trim()
.replace(/\s+/g, "-")
}
Result
[
{
title: "This is article title 1",
slug: "this-is-article-title-1"
},
...
]
Filter
also decouples the looping logic from your function, leaving behind a simpler construct where you are only focused on returning a predicate, a fancy way of saying Boolean
.
At each iteration, you simply check some value, and if that check returns true, the current value will be returned, otherwise, it will be discarded from the new array.
Imperative loop
const articles = [
{ title: "This is article title 1" },
{ title: "This is article title 2" },
{ title: "This is article title 3" }
];
const filteredTitles = [];
for (let i = 0; i < articles.length; i++) {
const article = articles[i];
const title = article.title;
const number = Number(title.match(/\d+$/));
if (number > 2) {
filteredTitles.push(title);
}
}
Result
[
{
title: "This is article title 3",
slug: "this-is-article-title-3"
},
...
]
Functional fix
const articles = [
{ title: "This is article title 1" },
{ title: "This is article title 2" },
{ title: "This is article title 3" }
];
const filteredTitles = articles
.filter(article => Number(article.title.match(/\d+$/)) > 2)
.map(article => article.title);
Result
[
{
title: "This is article title 3",
slug: "this-is-article-title-3"
},
...
]
Reduce
is by far the most powerful of the “big 3”. Its job is to “massage” an array into a new form. It could reduce that array down, like a filter, but it can also “build up” a new array. Under the hood, map
, filter
, and its two cousins every
and some
- are built from the reduce
call.
Some good uses include building up a new object of keys, a new array, a string, and for summing values. And by the way, it has little to do with the “reducer” pattern.
All three of these function calls can be used to clean up and decouple (and simplify) your code, reduce bugs, and can make for much more concise code.
Imperative loop
const titles = [
"This is article title 1",
"This is article title 2",
"This is article title 3"
];
const transformedArticles = [];
for (let i = 0; i < titles.length; i++) {
const title = titles[i];
const slug = createSlug(title);
const transformedArticle = { title, slug: article.title.toLowerCase()
.replace(/[^\w\s]/gi, "")
.trim()
.replace(/\s+/g, "-") }) };
transformedArticles.push(transformedArticle);
}
Result
[
{
title: "This is article title 1",
slug: "this-is-article-title-1"
},
...
]
Functional fix
const titles = [
"This is article title 1",
"This is article title 2",
"This is article title 3"
];
const transformedArticles = titles.reduce((acc, title) => (
{ title, slug: createSlug(title) }
), []);
function createSlug(title) {
return title
.toLowerCase()
.replace(/[^\w\s]/gi, "")
.trim()
.replace(/\s+/g, "-");
}
Result
[
{
title: "This is article title 1",
slug: "this-is-article-title-1"
},
...
]
We can also use the same reduce
function to build up a string, an object, or a sum of numbers!
When you are trying to follow the execution of a code block and all the variables are being changed and reassigned continuously, it makes it incredibly difficult to keep track of any one value at any specific point in time.
Const protects you from this, and it also helps in reducing the likelihood of a variable changing its type or “interface”. Interface is simply the functions that be called when accessing the variable. An Integer
has different functions available to it than you will find on an Array
or a String
.
This will reduce “type” errors, something that has plagued JavaScripters enough that a new subset language was invented to solve the problem, namely “TypeScript”. I think that solution is a bit like hammering a nail in with a sledgehammer, but that is for a future article.
Imperative code
let name = "Mark"
... 7 lines of code
name = "Marcus".length
name.split("")
Result
Uncaught TypeError: name.split is not a function
Functional fix
const name = "Mark"
... 7 lines of code
name = "Marcus".length
name.split("")
Result
Uncaught TypeError: Assignment to constant variable.
We never have to debug why the split
function fails, because as soon as we try to reassign the variable, we are alerted! No more trying to figure out what is this variable now.
Short functions are less prone to being dumping grounds for unstructured code. If you are concerned with the number of lines in your functions, you will be incentivized to extract additional behavior out instead of just “jamming it in”.
This will lead to well-structured, easier-to-read, and generally, better code. And you get all of this without having to put an incredible amount of thought into the “why”.
I will revisit the first example:
Imperative loop
const articles = [
{ title: "This is article title 1" },
{ title: "This is article title 2" },
{ title: "This is article title 3" }
];
function addSlugs(articles) {
const articlesWithSlugs = [];
for (let i = 0; i < articles.length; i++) {
const article = articles[i];
articlesWithSlugs.push({ ...article, slug: article
.title.toLowerCase()
.replace(/[^\w\s]/gi, "")
.trim()
.replace(/\s+/g, "-") });
}
return articlesWithSlugs
}
Functional fix
const articles = [
{ title: "This is article title 1" },
{ title: "This is article title 2" },
{ title: "This is article title 3" }
];
function slugArticles(articles) {
return articles.map(article => ({
...article,
slug: slug(article.text)
}))
}
function slug(text) {
return text.toLowerCase()
.replace(/[^\w\s]/gi, "")
.trim()
.replace(/\s+/g, "-")
}
I think we can both agree that the detangling of the loop from the “work to be done” and the extraction of the slug
function are both wins!
By ensuring that every function returns a value, you will never have to debug, step by step, line by line - code, to figure out why the code “loses its value” - “somewhere”. How many times have you dumped console.log
statements all over your code?
Ifs are divine. If/Else/Elsif is evil.
If you write an If/Else - do not be surprised if someone (maybe you, tomorrow) will come in behind you and add a nested If/Else inside the original if
statement.
Then the same thing will happen again, this time, in the else
side of the conditional. Before you know it, you have an insane cyclical complexity score, and your code has become a “spaceship” - where, if you turn your monitor on the side, it will resemble a spaceship from an old video game.
This makes code difficult to read, and it makes it possible for all kinds of logic to creep in.
Forgive me for my sin, but here is an example:
Gross code
function evaluateCondition(a, b, c, d, e) {
if (a) {
// First condition is true
if (b) {
// Nested condition: a and b are true
if (c) {
// Nested condition: a, b, and c are true
if (d) {
// Nested condition: a, b, c, and d are true
if (e) {
// All conditions are true
console.log("All conditions are true");
} else {
console.log("Condition e is false");
}
} else {
console.log("Condition d is false");
}
} else {
console.log("Condition c is false");
}
} else {
console.log("Condition b is false");
}
} else if (c) {
// First condition is false, but c is true
console.log("Condition a is false, but c is true");
} else if (d) {
// First and second conditions are false, but d is true
console.log("Conditions a and c are false, but d is true");
} else {
// All conditions are false
console.log("All conditions are false");
}
}
Let’s not make spaceships, let’s write pretty code:
function evaluateCondition(a, b, c, d, e) {
if (!a) return "All conditions are false";
if (!b) return "Condition b is false";
if (!c) return "Condition c is false";
if (!d) return "Condition d is false";
if (!e) return "Condition e is false";
return "All conditions are true";
}
The rules work like this:
Use if
for guard clauses and always return from them
Use a ternary
when the conditional is binary (2 branches)
Use a switch
statement for anything with complex branching
This brings me to switch
rules!
The switch
is a great construct when used well, and it is a nest of vipers when it is not.
Never use the break
keyword. It allows fallthrough behavior and makes for easy bugs. Instead, every case
statement should end with a return “some value”. The switch can then be put into a function, which makes it more functional and less imperative.
Always include a default case, I usually end all my switches with a default case that throws an Error
telling the caller that no case matched!
I have found this use of switch-in-a-function not only achieves functional bliss, but greatly reduces the caller’s size, and reduces unwanted logic errors and bugs.
Gross switch
function evaluateCondition(value) {
let retval
switch (value) {
case 1:
retval = "Value is 1"
break;
case 2:
retval = "Value is 2"
break;
case 3:
retval = "Value is 3"
break;
}
return retval
}
Beautiful switch
function evaluateCondition(value) {
switch (value) {
case 1:
return "Value is 1";
case 2:
return "Value is 2";
case 3:
return "Value is 3";
default: throw Error("No case matched!")
}
}
In the article, I have highlighted several key tips for improving the readability and maintainability of JavaScript code. These rules are influenced by experienced programmers such as Douglas Crockford, Gary Bernhardt, Rich Hickey, and others. In this part of the series, the author focuses on functional programming and how to use it effectively:
By following these guidelines, programmers can write clean, concise, and human-readable JavaScript code, making it easier for themselves and others to understand and work with the code.