Control Structures and Error Handling: #CleanCode P5

Control Structures and Error Handling: #CleanCode P5

Hello Friends😎, and Hi Chubby🙋🏽‍♂️, It has been a while since I met you in blogs.🤭

In this blog, 👩🏾‍💻we're covering the in-depth concepts of control structures🤹🏽‍♂️ used in programming and how we can use them for efficient code. Previously we have covered:

  1. Why does clean code matter, in CleanCodeP1

  2. How good names will make your life easy, in CleanCodeP2

  3. How comments can be your best buddy, in CleanCodeP3

  4. Functions and Methods are the souls and how to make them clean and efficient CleanCodeP4

So let's start this article.

Alert: This article contains heavy codes. If you are unfamiliar with Javascript or similar scripting language, then just close your eyes and like this article.

Control Structures or Conditionals: ✋🏽

Control structures and conditionals determine the flow🛥 of the program. According to GFG, Any algorithm or program can be more clear and understood if they use self-contained modules called logic or control structures. There are three basic types of logic, or flow of control, known:

  1. Sequence logic, or sequential flow➡ (flow depends on the series of instructions)

  2. Selection logic, or conditional flow🔜 (single, double or multiple conditions)

  3. Iteration logic, or repetitive flow 🔄(logic employs a loop that involves a repeat statement till the condition does not meet)

All the code we write in a file, from left to right and top to bottom are parsed 🛐by the parser and compiled as they are written, hence this series of instruction follow the sequential flow.

When we encounter multiple directional flows♿ like in authentications, payment gateways,💷 and user interactions🎭 where we have to give multiple directions to the visitor according to the choices and errors made by the visitors. Here we use conditionals like if-else, else-if, and nested if-else.

When conditions are iterative and follow sequential iterations to fulfil the condition, then we require iterative or repetitive💫 conditionals loops that loop over all the possible choices and return that choice that meets the required condition. Here we use for-loop or while-loop.

Traps of these conditionals:🕳

Since these conditionals involve too many choices which we are not aware of. If we use such a loop♻ without giving proper conditions to end the loop, it will fall into a trap of conditions. The system will repetitively run the loop and not return any result. So before setting the looping boundaries one has to define the condition when the loop should return the value.

How can we make the control structure clean and efficient?💈

There are several techniques and best practices to make the functions involving control structures clean and understandable. Some of these are:

  1. Avoid deeply nested ☔conditionals. With every nesting, Big "O" of the function/ algorithm becomes complex and inefficient.

  2. Use of Factory functions and Polymorphism🖨 to avoid nested loops.

  3. Here also Naming🈲 the if-checks will help in understanding the code and using the right check (prefer Positive checks over negative checks).

  4. Utilize Errors to avoid nesting.🆗

Deep Nesting and Guards:👨🏻‍✈️

If we consider some nested if-else statements and try to understand the concept of Guard, it would be easier.

if(email.includes('@')) {     // avoid this
    // do stuff
}

if(!email.includes('@')) {     // do this  // act as a guard to Next codes
   return;
}
// do stuff

/// OR
if(user.active) {                 // Avoid this
    if(user.hasPurchases()){
        // do stuff
    }
}

if(!user.hasPurchases()){   // act as a Guard
    return;
}

if(user.active) {                 // Avoid this
   // do stuff
}

Through guards, we use sequential flow which is the most efficient flow of 🗑programming. By using the negative check as a guard for important codes, we can return as soon as the condition is not met. Below codes will not run after.

It is a saying: Use Guard and fail fast to avoid deep involvement in the fail function.

Eliminate nesting by guarding the valuable codes:✋🏽

This is the best and most efficient way to avoid nesting also. We can fail that condition first which is more deeply connected🤼‍♂️ to the outer conditions. By doing that we avoid executing unnecessary codes which ultimately be void if the nested condition does not meet.

// if we consider this function: 
function processTransactions(transactions) {
  if (transactions && transactions.length > 0) {
    for (const transaction of transactions) {
      if (transaction.type === 'PAYMENT') {
        if (transaction.status === 'OPEN') {
          if (transaction.method === 'CREDIT CARD') {
            processCreditCardPayment(transaction);
          } else if (transaction.method === 'PAYPAL') {
            processPaypalPayment(transaction);
          } else if (transaction.method === 'PLAN') {
            processPlanPayment(transaction);
          }
        } else {
          console.log('Invalid transaction type!');
        }
      } else if (transaction.type === 'REFUND') {
        if (transaction.status === 'OPEN') {
          if (transaction.method === 'CREDIT CARD') {
            processCreditCardRefund(transaction);
          } else if (transaction.method === 'PAYPAL') {
            processPaypalRefund(transaction);
          } else if (transaction.method === 'PLAN') {
            processPlanRefund(transaction);
          }
        } else {
          console.log('Invalid transaction type: ' + transaction);
        }
      } else {
        console.log('invalid transaction type' + transaction);
      }
    }
  } else {
    console.log('No transactions to process');
  }
}

We can change this Deeply nested code by using just the guard concept, into:

function processTransactions(transactions) {
  if (!transactions || transactions.length === 0) {
    console.log('No transactions to process');
    return;
  }

  for (const transaction of transactions) {
    if (transaction.status !== 'OPEN') {
      console.log('Invalid transaction type!');
      continue;  // continue to next iteration
    }
    if (transaction.type !== 'PAYMENT') {
      console.log('invalid transaction type' + transaction);
      continue; // continue to next iteration
    }

    if (transaction.method === 'CREDIT CARD') {
      processCreditCardPayment(transaction);
    } else if (transaction.method === 'PAYPAL') {
      processPaypalPayment(transaction);
    } else if (transaction.method === 'PLAN') {
      processPlanPayment(transaction);
    }

    if (transaction.type !== 'REFUND') {
      console.log('invalid transaction type' + transaction);
      continue;
    }

    if (transaction.method === 'CREDIT CARD') {
      processCreditCardRefund(transaction);
    } else if (transaction.method === 'PAYPAL') {
      processPaypalRefund(transaction);
    } else if (transaction.method === 'PLAN') {
      processPlanRefund(transaction);
    }
  }
}

However, here we encounter a mixture🍹 of levels of abstraction, which we have learned in the previous blog, that a function should contain a similar level of abstraction to make it more efficient. Here custom functions are of high order functions🐱‍🏍 but also have a low level of functions like console.log, and if-statement checks inside the function.

Positive Checks and Refactoring: 🛒

Since it depends on where we should use positive checks and where the negative ones are. So before deciding on this, we should consider the actual condition where we are going to use this check. If the statement is doing a "boolean" ☯check, then use "Positive Check" otherwise to avoid nesting, use negative checks as we have used in the previous code block. So if we refactor our code more based on abstraction levels, and checks,

function processTransactions(transactions) {
  if (isEmpty(transactions)) {
    showErrorMessage('No transactions to process');
    return;
  }

  for (const transaction of transactions) {
    processSingle(transaction);
  }
}

function processSingle(transaction) {
  if (!isOpen(transaction)) {
    showErrorMessage('Invalid transaction type!');
    return;
  }

  if (isPayment(transaction)) {
    processPaymentType(transaction);
  } else if (isRefund(transaction)) {
    processRefundType(transaction);
  } else {
    showErrorMessage('invalid transaction type' + transaction);
    return;
  }
}

function processPaymentType(transaction) {
  if (transaction.method === 'CREDIT CARD') {
    processCreditCardPayment(transaction);
  } else if (transaction.method === 'PAYPAL') {
    processPaypalPayment(transaction);
  } else if (transaction.method === 'PLAN') {
    processPlanPayment(transaction);
  }
}

function processRefundType(transaction) {
  if (transaction.method === 'CREDIT CARD') {
    processCreditCardRefund(transaction);
  } else if (transaction.method === 'PAYPAL') {
    processPaypalRefund(transaction);
  } else if (transaction.method === 'PLAN') {
    processPlanRefund(transaction);
  }
}

function isOpen(transaction) {
  return transaction.status !== 'OPEN';
}

function isRefund(transaction) {
  return transaction.type === 'REFUND';
}

function isPayment(transaction) {
  return transaction.type === 'PAYMENT';
}

function isEmpty(transactions) {
  // will return boolean value:Positive Check
  return !transactions || transactions.length === 0;
}

function showErrorMessage(message, item) {
  console.log(message);
  if (item) {
    console.log(message, item);
  }
}

This code is way cleaner than the previous one and has arranged in similar levels of abstraction, hence becoming more efficient. We have made a couple of small functions for the trivial tasks😟 which we do not go through during the review of the main function. We can now easily understand the functions, and what exactly they are doing. The long program file 📃is not a problem against the decluttered codes.

Inversion of Condition to make it more targetted:🎯

Since we might encounter code duplications and several repetitive flows which are very annoying to refactor. If we see the previous code block, processPaymentTypeTransaction() and processRefundTypeTransaction() have very similar code flow of instructions, even though they direct🐱‍🏍 to different functions. We can reduce the duplication by rearranging the level of abstraction of if-checks and inverting the conditions.🤷🏽‍♂️ What??

In code, we're first checking whether the transaction is of paymentType or refundType, and then we're checking whether we use a credit card, PayPal, or Plan method for the transaction. If we invert this condition and first check for the method of transaction and then the payment type.

// define transaction method
function usesTransactionMethod(transaction, method){
    return transaction.method === method;
}
// check of payment
if(usesTransactionMethod(transaction, 'CREDIT_CARD')){
    processCreditCardPayment(transaction);
} else if (usesTransactionMethod(transaction, 'PAYPAL')) {
    processPaypalPayment(transaction);
} else if (usesTransactionMethod(transaction, 'PLAN')) {
    processPlanPayment(transaction);
}
// check for type of payment

function processCreditCardPayment(transaction) {
    if(isRefund(transaction)) {
        processCreditCardRefund(transaction);
    } else if (isPayment(transaction)) {
        processCreditCardPayment(transaction);
    } else {
       showErrorMessage('invalid transaction type' + transaction);
       return; 
    }
}
// similarly for Paypal and Plan type.. 
// This will reduce the level of abstraction of SingleTransaction() and elevate for paymentType(). Over all it will maintain the abstraction and reduce cognitive load of the coder to understand the code in review process.

Utilizing "Errors" in guarding and checking the Code:😐

Since errors are nothing but checks on whether our program flow is in the right direction or not. Through errors, we can check where we need proper sanitization🦠 of code and guards. Throwing and handling errors are also used as alternatives to the if-else statement and making more focused functions.

Simple rule: if something is an error => Make it an error, don't try to solve it with an if-statement.

if(!isEmail) {
    return {code: 422, message: "Invalid Credentials"}  // json object
}
// OR
if(!isEmail) {          // Better way : Modern Way
    const error = new Error("Invalid Credentials");
    error.code = 422;
    throw error;    // throw will terminate the function and require handling
}

Since functions should do specific tasks for which they are meant, if they also include error handling and checks, will make functions very bulky 🤢and annoying. For that purpose, we usually export those codes by refactoring them by making small functions. But errors should be handled in the right positions of the flow of the program. This can be done by using a try-catch or try-accept statements for proper error handling for "throw"🤮 statements.

// in Main transaction function:
function processTransactions(transactions) {
    if (isEmpty(transactions)) {             // Error Guard
        showErrorMessage('No transactions to process');
        return;
    }
    for (const transaction of transactions) {
      try {                            
        processSingle(transaction);
      } catch (error) {
        showErrorMessage(error.message);
      } 
    }        
  }
}
// inside singleTransaction fucntion
function processSingle(transaction) {
  if (!isOpen(transaction)) {
    const error = new Error('Invalid Transaction Type!');
     error.item = transaction;
     throw error;
  }

  if (!isPayment(transaction) &&  !isRefund(transaction)) {  // Error Guard
     const error = new Error('Invalid Transaction Type!');
     error.item = transaction;
     throw error;
  }
  if (isPayment(transaction)) {
    processPaymentType(transaction);
  } else if (isRefund(transaction)) {
    processRefundType(transaction);
  } else {
    showErrorMessage('invalid transaction type' + transaction);
    return;
  }
}
// then all other small functions...

Use of Validation Functions to refactor code:👀

Since we have refactored a lot, but since we can see our main function and single transaction functions are still handling validation logics👁 which are not mandatory to do inside the main function, we can extract them out from both of these functions so that our code becomes more iterable and clean.

function processTransactions(transactions) {
    validateTransactions(transactions);  // refactored
    for (const transaction of transactions) {
      try {                            
        processSingle(transaction);
      } catch (error) {
        showErrorMessage(error.message);
      } 
    }        
  }
}
function processSingle(transaction) { 
  validateSingleTransaction(transaction);
  processByMethod(transaction);
  } else {
    showErrorMessage('invalid transaction type' + transaction);
    return;
  }
}

function validateTransactions(transactions) {
  if (isEmpty(transactions)) {             // Error Guard
     errorHandler('Invalid Transaction Type!', transaction);
  }    
}

function validateSingleTransaction(transaction) {
  if (!isOpen(transaction)) {
     errorHandler('Invalid Transaction Type!', transaction);
  }

  if (!isPayment(transaction) && !isRefund(transaction)) {  // Error Guard
     errorHandler('Invalid Transaction Type!', transaction);
  }
}

function errorHandler(message, item) {
     const error = new Error(message);
     error.item = item;
     throw error;
}
// and some small functions

Hence we have made our original code way leaner and clean. It is a personal preference of the coder how s/he likes the code, but in a clean code world, refactored and code splitting are considered best practices in terms of efficient code.

Factory Functions and Polymorphism:🛒

In codes, we encounter many functions just to create or produce any object or data. Their main object is to produce output from an input. Those functions are referred to as Factory functions 🎲as they are factories to the data/objects. Polymorphism is the phenomenon of using an object for different purposes. Factory functions are used to produce data from different inputs but functions remain the same.

Polymorphism is used for Class objects 🎲where we instantiate a class object and then use the instance repetitively for different purposes. We'll talk more about this in the next blog. Here I want to introduce it just to make a clear picture🖼 of the phenomenon where we can reduce repetitive codes and use a function to generate different outputs without creating similar functions again and again.

// here, our transaction types are payment and refund which again get with credit card, paypal, plan methods. By creating and object for them, we can reduce many repetitive functions...
function getTransactionProcessor(transaction) {
    let processors = {
        processPayment : null,
        processRefund : null,   
    };
// point to function method related to creditcard, paypal, and plan.
    if(usesTransactionMethod(transaction, 'CREDIT_CARD')){
        processors.processPayment = processCreditCardPayment;  
        processors.processRefund = processCreditCardRefund;
    } else if (usesTransactionMethod(transaction, 'PAYPAL')) {
        processors.processPayment = processPaypalPayment;
        processors.processRefund = processPaypalRefund;
    } else if (usesTransactionMethod(transaction, 'PLAN')) {
        processors.processPayment = processPlanPayment;
        processors.processRefund = processPlanRefund;
    }
    return processors;
// return the map of the functions which we can use where ever we like according to the parameters.
}

This factory function has replaced the following functions:

function processByMethod();
function processCreditCardTransaction();
function processPaypalTransaction();
function processPlanTransaction();

And reduced the overall transaction flow into a clean code:

function processTransactions(transactions) {
    validateTransactions(transactions);  // refactored
    for (const transaction of transactions) {
      try {                            
        processSingle(transaction);
      } catch (error) {
         errorHandler('Invalid Transaction Type!', transaction);
      } 
    }        
  }
}
function processSingle(transaction) { 
   try {  
    validateSingleTransaction(transaction);
    const Processors = getTransactionProcessors(transaction);
    if(isPayment(transaction)) {
        processors.payment(transaction);
    } else {
        processors.refund(transaction);
    }
  } catch {
     errorHandler('Invalid Transaction Type!', transaction);
    return;
  }
}

function validateTransactions(transactions) {
  if (isEmpty(transactions)) {             // Error Guard
     errorHandler('Invalid Transaction Type!', transaction);
  }    
}

function validateSingleTransaction(transaction) {
  if (!isOpen(transaction)) {
     errorHandler('Invalid Transaction Type!', transaction);
  }

  if (!isPayment(transaction) && !isRefund(transaction)) {  // Error Guard
     errorHandler('Invalid Transaction Type!', transaction);
  }
}
function getTransactionProcessors(transaction) {
    let processors = {
        processPayment : null,
        processRefund : null,   
    };
// point to function method related to creditcard, paypal, and plan.
    if(usesTransactionMethod(transaction, 'CREDIT_CARD')){
        processors.processPayment = processCreditCardPayment;  
        processors.processRefund = processCreditCardRefund;
    } else if (usesTransactionMethod(transaction, 'PAYPAL')) {
        processors.processPayment = processPaypalPayment;
        processors.processRefund = processPaypalRefund;
    } else if (usesTransactionMethod(transaction, 'PLAN')) {
        processors.processPayment = processPlanPayment;
        processors.processRefund = processPlanRefund;
    }
    return processors;
}

function usesTransactionMethod(transaction, method){
    return transaction.method === method;
}

function errorHandler(message, item) {
     const error = new Error(message);
     error.item = item;
     throw error;
}

This is the final code refactored in different phases🎚 as we have seen. I admit that it became a very long file with lots of functions and full of jumping from one function to another but we can make them shorter by splitting them into different files📃 according to their use cases. Then we can import/ require them and use them accordingly and hence make our main transaction function file clean. But this is also based on preference. 👨🏻‍✈️

Let's Summarise: Yeah it is the last paragraph🥱

We have started our discussion with the Control structure overview, types, and how and where we use them. Then we have taken a nested-decluttered🤢 example which we have refactored with learning to use positive and negative checks, the use of if-statement as a guard and the inversion of conditions. We also learned how we can use errors as a guard.👨🏻‍✈️ Error handling become easy and structured and as we are done with the refactoring we learned about factory functions🎲 that can be used to create function objects 🛒to get the data/ object according to the input we give and use them according to our needs.

We have done all of this by using our previous knowledge of naming of functions, creation of meaningful functions, splitting of code and many more concepts.

What will be in the next blog?

We'll talk about:

  1. Classes and Objects🎲

  2. How we can make our classes and objects clean, bug-free🦠 and maintainable?

  3. We can easily differentiate between classes, objects, data structures or containers.

  4. We'll go through object-oriented principles( SOLID,🦾 law of Demeter),

  5. and polymorphism, which we have taken a glance at in this article already.

I know this blog was lengthy and required deep focus. If you like this blog, please like and share it with others. You can connect with me on Linkedin . So, with this, I'll meet you in the next blog. Byeee🙋🏽‍♂️