To make code easy to read and maintain, we should follow some best practices.
In this article, we’ll look at some best practices we should follow to make everyone’s lives easier.
Interface Segregation Principle
Even though JavaScript doesn’t have explicit interfaces, we shouldn’t make outside code depend on interfaces they don’t use.
We should create interfaces that they all can use.
For instance, instead of writing classes with lots of methods, we break them up into smaller ones.
This also keeps them from having more than one responsibility.
Testing
Testing is important to keep the quality of the software.
We can automate most of them to make our work easier.
There is something that we should be aware of.
Single Concept Per Test
We should keep out tests simple by testing one concept in each test.
For instance, instead of writing:
import assert from "assert";
describe("Rectangle", () => {
it("handles dimension changes", () => {
let rectangle;
rectangle = new Rectangle(1, 2);
rectangle.setWidth(30);
assert.equal(30, rectangle.getWidth());
rectangle = new Rectangle(1, 2);
rectangle.setHeight(30);
assert.equal(30, rectangle.getHeight());
});
});
We write:
import assert from "assert";
describe("Rectangle", () => {
it("changes height", () => {
const rectangle = new Rectangle(1, 2);
rectangle.setWidth(30);
assert.equal(30, rectangle.getWidth());
});
it("changes width", () => {
const rectangle = new Rectangle(1, 2);
rectangle.setHeight(30);
assert.equal(30, rectangle.getHeight());
});
});
We create 2 tests instead of one.
Async
JavaScript code is async, so we’ll have to write async callbacks a lot.
Use Promises, not Callbacks
We should use promises to do async operations.
It’s especially useful if we do have to do multiple operations.
For instance, we write:
import { writeFile, readFile } from "fs";
readFile(
`'./foo.txt',`
(`err, data)`) => {
if (err) {
console.error(err);
} else {
writeFile("./bar.txt", `data`, writeErr => {
if (writeErr) {
console.error(writeErr);
} else {
console.log("File written");
}
});
}
}
);
We write:
const fs = require("fs");
const { promisify } = require("util");
const writeFile = promisify(fs.writeFile);
const readFile = promisify(fs.readFile);
`async function main() {
const data = await writeFile("/foo.txt");
await writeFile("./`bar.txt`", data);
}`
We use async
and await
to chain the promises.
util.promisify
convert some async functions to promises.
Error Handling
Error handling is important.
We shouldn’t ignore caught errors.
For instance, instead of writing:
try {
doSomething();
} catch (error) {
console.log(error);
}
We write:
try {
doSomething();
} catch (error) {
console.error(error);
notifyError(error);
}
We make the error obvious with console.error
or another function or both.
Don’t Ignore Rejected Promises
We shouldn’t ignore promises that are rejected.
For instance, instead of writing:
getdata()
.then(data => {
doSomething(data);
})
.catch(error => {
console.log(error);
});
We should write:
getdata()
.then(data => {
doSomething(data);
})
.catch(error => {
console.error(error);
notifyError(error);
});
We make the error obvious with console.error
or another function or both.
Formatting
We shouldn’t have to argue over formatting.
It’s a waste of time.
Use Consistent Capitalization
We should use consistent capitalization for our code.
For instance, we capitalize names of constants:
const DAYS_IN_WEEK = 7;
Also, we use camel case for most other identifiers:
function eraseDatabase() {}
And we use Pascal case for constructors or classes:
class Animal {}
Function Callers and Callees Should be Close
We should keep the callers and callees close together so we don’t have to jump around to read our code.
For instance, we can write:
class Foo {
foo(){}
bar() {
this.foo();
}
}
We keep them together as close as we possibly can.
This is better than:
class Foo {
foo(){}
baz(){}
qux(){}
bar() {
this.foo();
}
}
We’ve to jump through multiple methods to read the stuff.
Comments
We should comment on things that can’t be told by the code.
For instance, instead of writing:
// loop through objects
for (const item of items) {
//...
}
We can omit it since it’s the code already tells us what it does:
for (const item of items) {
//...
}
Conclusion
We should have small interfaces so that we don’t have to depend on big interfaces where the items aren’t completely used.
Tests should have a single concept.
Use promises for async code.
Function callers and callees should be close together.
And we shouldn’t write comments to repeat what is said in the code.