Why It is Important to Monitor Code Quality

Written by olgakiba | Published 2022/12/12
Tech Story Tags: code-quality | code | coding | coding-skills | programming | optimization | team-productivity | teamwork

TLDRQuality of code is directly connected with the following qualities: readability, controllability, readability and controlability. Linear code - code that can be read from top to bottom without having to go back to previously read code. Simple algorithm example: doOnWhatever; doThird(); doThird; doSecond; doWhatever;. DoThird: doThird(); doFourth: doSomething; doSomething: doWhatever. doWhatever. DoSomething: If (something) doFirst;. If (whatever) doSomething? doSomething. If something else is something, doSomething is something? If something is something it is something else, doIt is something. If it is a function, it is not a function it will be a functionvia the TL;DR App

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?

Things that determine code quality

Writing sequence

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:

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:

  1. 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.

  2. 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();
    }
    

  3. 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;
    

  4. 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);
    

  5. 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, ‘‘.

  6. 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
    

  7. 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.

Naming

  1. Using camelCase notation:

    const getSomeValue = () => {};
    
  2. Not using transliteration if your company/project language not English:

    const tovar = {} // example from Russian: tovar = product
    
  3. 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
    
  4. 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);
    
  5. Calling variables speaking names:

    const getProducts = () => {};
    const addProductToCart = () => {};
    

Declaratively

The declarative coding style has a number of advantages over the imperative style:

  • code is easier to read
  • code is easier to maintain
  • complex constructs are hidden behind methods and abstractions

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

Modularity

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.

Code style

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.

Repository

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

Conclusion

Summing up the article, here are the following tips that you should follow for writing quality code:

  • keep your code linear
  • reduce nesting
  • use speaking names
  • strive for declaratively and modularity
  • get code style
  • pay attention to git flow and commit messages
  • use linters, formatters as helpers in your project


Written by olgakiba | Love Chinese culture and coding
Published by HackerNoon on 2022/12/12