Usually, when we are working on a particular software product, the quality of the code is not our first concern. Performance, functionality, the stability of its operation, etc. are much more important to us.
But is the quality of the code a factor that positively impacts the above indicators? My answer is yes because such a code is directly connected with the following qualities:
readability - the ability to look at the code and quickly understand the implemented algorithm, and evaluate how the program will behave in a particular case.
controllability - the ability to make the required amendments to the code in the shortest possible time, while avoiding various unpleasant predictable and unpredictable consequences.
Code is a book that is written by one author and is supplemented by other authors. It will be passed through different people and what the reader gets out of this book will depend on how the code is written. So it is pretty important, isn’t it?
As we illustrated above, code is a book and as such, it should be written in a linear style.
Linear code - code that can be read from top to bottom without having to go back to previously read code.
For example, a perfectly linear snippet:
{
doFirst();
doSecond();
doThird();
}
And not linear at all:
{
if (something) {
doFirst();
} else {
if (whatever) {
if (a) {
if (b) {
doSecond();
}
}
}
doThird();
}
}
Let’s try to fix it. Here we can move complex sub-scripts into separate functions:
{
const doOnWhatever = () => {
if (a) {
if (b) {
doSecond();
}
}
}
if (something) {
doFirst();
} else {
if (whatever) {
doOnWhatever();
}
doThird();
}
}
First of all, you need to present your logic as a flowchart as simply as possible:
" alt="Image 1. Simple algorithm example">
Carefully go through this scheme and try to transfer it to the code, avoiding very large nesting.
There are some tips on how to handle large nesting:
Using break, continue, return or throw to get rid of the else block:
{
const doOnWhatever = () => {
if (a) {
if (b) {
doSecond();
}
}
}
if (something) {
doFirst();
return;
}
if (whatever) {
doOnWhatever();
}
doThird();
}
It would be incorrect to conclude that you should never use the else statement at all. Firstly, the context does not always allow you to put ‘break, continue, return or throw’. Secondly, the value of this may not be as obvious as in the example above, and a simple ‘else’ will look much simpler and clearer than anything else. And finally, there are certain costs when using multiple returns in functions, because of which many generally regard this approach as an anti-pattern.
Combining nested if-s:
{
const doOnWhatever = () => {
if (a && b) { // here we combined "a" and "b" conditions
doSecond();
}
}
if (something) {
doFirst();
return;
}
if (whatever) {
doOnWhatever();
}
doThird();
}
Using the ternary operator (a? b: c) instead of if:
let something;
if (a) {
something = b;
} else {
something = c;
}
const something = a ? b : c;
const something = a ? b : aa ? c : d;
Eliminating code duplication:
const a = new Object();
doFirst(a);
doSecond(a);
const b = new Object();
doFirst(b);
doSecond(b);
const workWithObject = (x) => {
doFirst(x);
doSecond(x);
}
const a = new Object();
const b = new Object();
workWithObject(a);
workWithObject(b);
Simplifying code:
if (obj != null && obj != undefined && obj != '') {
// do something
}
if (obj) {
// do something
}
The fact is that thanks to the implicit cast to boolean, the if (obj) {} check will filter out: false, null, undefined, 0, ‘‘.
Not creating variables that you can work without:
...
const ERROR = 1
const sum = getSum();
const sumWithError = sum + ERROR;
doSomething(sumWithError);
...
doSomething(getSum() + ERROR);
This situation is also called "creating a variable to create a variable". Variables should help readers understand code quickly and not slow them down forcing them to read unnecessary text.
Here are examples of necessary naming:
const PHONE_NUMBER_REGEX = /^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$/
str.match(PHONE_NUMBER_REGEX) // this helps us to understand that we are looking for a phone number
const LUCK_ERROR = 10%;
const result = luckPercent - LUCK_ERROR // this helps us to avoid working with magical numbers
Using encapsulation
Creating private data through closure:
const createCounter = () => {
let count = 0;
return ({
click: () => count += 1,
getCount: () => count
});
};
const counter = createCounter();
counter.click();
counter.click();
counter.click();
console.log(
counter.getCount()
);
Here we used a method that has access to private data inside the scope (lexical environment) of the function. These functions and methods have reference-based access to variables within the function, even after the function has been completed. These references are live, so if the state changes in the inner function, the changes are propagated to each privileged function. In other words, when we call counter.click(), it changes the value that counter.getCount() sees.
Creating private data through private fields:
class Counter {
#count = 0
click () {
this.#count += 1;
}
getCount () {
return this.#count.toLocaleString()
}
}
const myCounter = new Counter();
myCounter.click();
myCounter.click();
myCounter.click();
console.log(
myCounter.getCount()
);
New class fields are much better than underscores because they don't rely on convention but provide true encapsulation.
Using camelCase notation:
const getSomeValue = () => {};
Not using transliteration if your company/project language not English:
const tovar = {} // example from Russian: tovar = product
Avoiding of using abstract naming:
const getProductNames = products.map(item => item.name) // less readable code
const getProductNames = products.map(product => product.name) // more readable code
Naming constants in capital letters:
const BANNER_WIDTH = 300
Typically, uppercase letters for naming constants or variables are used when the value is known before the script is executed and is written directly to the code, for example:
const BIRTHDAY = '4/18/1982';
Use capital letters If the variable is evaluated during script execution, then lower case is used:
const age = someCode(BIRTHDAY);
Calling variables speaking names:
const getProducts = () => {};
const addProductToCart = () => {};
The declarative coding style has a number of advantages over the imperative style:
Example of comparing imperative code and declarative:
for(let i = 0; i < textArr.length; i++) {
if(arr[i] === 'Text to console log') {
console.log(arr[i])
}
} // imperative code
textArr.filter(text => text === 'Text to console log').map(text => console.log(text)); // declarative code
A good practice is to split code into modules. Such code increases readability by separating the code into abstractions so it helps to hide hard-to-read code. Also, code is easier to test and easier to find errors accordingly.
So let’s see how to implement it by using the previous code:
const createCounter = () => {
let count = 0;
return ({
click: () => count += 1,
getCount: () => count
});
}; // this part of code can be moved into separated module
const counter = createCounter(); // this can be used inplace where it's needed because it's not nesessary to see how counter was actually created
People don’t really need to see the whole code controlling a porcess and it’s quite fine to hide it behind a speaking name. Of course, sometimes you might have to supplement a functionality or see how it works. To do this you can freely go to the exact file to investigate it, but it happens not that often so no needs to keep it right in place of usage.
It is a set of rules/projects/conventions that developers must follow. It can be described somewhere, for example, wiki section in Gitlab with examples of what to do and not to do.
Here is an example of how I implement it in the project:
Moreover, you can take a ready-made style code and implement it in your project, for example, the Airbnb code style.
It is a successful repository management model when there are two main branches: develop, master and the rest are temporary branches.
Temporary branches should contain a type of change such as release, feature, bugfix, or hotfix and task ticket number:
fetaure/123
bugfix/321
When we start a new task, we go off a develop-branch. After passing the code review, we merge it back into the develop. Then, we collect releases from the develop branch link it to the release branch, and release it all to the master.
What message should be sent when we commit some changes? ‘fixed a bug‘, ‘added a feature‘ - are not good examples of messages.
A quality message is when it contains a capacious statement of the essence of change.
"added a banner component" // - commit message example
Summing up the article, here are the following tips that you should follow for writing quality code: