Control Structures and Errors By albro

Control Structures and Errors

Control structures are structures that control the execution logic of your program. The simplest of these structures is the if statement! One of the problems that occurs in writing clean code is the excessive use of these control structures (especially nested). Pay attention to the following code:

if (rowCount > rowIdx)
    {
      if (drc[rowIdx].Table.Columns.Contains("avalId"))
      {
        do
        {
          if (Attributes[attrVal.AttributeClassId] == null)
          {
            // do stuff
          }
          else
          {
            if (!(Attributes[attrVal.AttributeClassId] is ArrayList))
            {
              // do stuff
            }
            else
            {
              if (!isChecking)
              {
                // do stuff
              }
              else
              {
                // do stuff
              }
            }
          }
          rowIdx++;
        }
        while (rowIdx < rowCount && GetIdAsInt32(drc[rowIdx]) == Id);
      }
      else
        rowIdx++;
    }
    return rowIdx;
  }

Codes that have loops and different conditions and are written in this way are at the lowest level in terms of readability because it will take a lot of time to read and understand them. The above codes are called arrow code because their general appearance is similar to the tips of arrows:

if
  if
    if
      if
        do something
      endif
    endif
  endif
endif

In this post, I want to cover two main topics: preventing nested control structures and handling errors. The topics of this post are such that we will deal less with theory and directly enter into coding and edit our own codes many times, so try to be patient and study step by step. To write clean code related to control structures, you should remember the following points:

  • Avoid deep nesting. Deep nesting means excessive nesting. Doing this will usually create the same arrow codes that I showed you earlier.
  • Use techniques such as factory functions and polymorphism.
  • Try to use positive terms. For example, use isEmpty instead of isNotEmpty. Positive terms are always easier to read than negative terms.
  • Use error messages to organize your control structures.

What is Guard?

One of the best ways to shorten arrow code is to use a theme called Guards. To understand guards, it is better to use a practical example:

if (email.includes('@')) {
  // Do Anything
}

This code is a very simple code that only checks for the @ sign in the email variable. I have specified possible operations in the form of a comment (perform specific operations); That is, it doesn't matter what kind of operation we want to do, but we assume that the desired operation is very long and requires many control structures. In this case, we can use a guard; It means to reverse the condition:

if (!email.includes('@')) {
  return;
}


// perform special operations

Can we understand the working mechanism of the code from the code above? Here we have reversed the if condition and call it guard. Why? If the desired email does not have the @ symbol, the condition is established, so we enter it. At this time, the return statement causes us to exit the function and the rest of the code (the "perform special operations" section) will never be executed. The if condition is a guard because it protects its subsequent parts. Doing this will make us have consecutive codes instead of nested codes. The more nested the codes are, the harder it will be to read them. For example:

if (user.active) {
  if (user.hasPurchases()) {
    // perform special operations
  }
}

In this example, we have two nested if conditions and it is annoying to read them. If we turn the inner condition into a guard, we can increase the readability of the code:

if (!user.hasPurchases()) {
  return;
}


if (user.active) { // perform special operations }

By doing this we have avoided nested if conditions. Our assumption is that this code is part of a function, so when we reach the return statement, we exit the function and we will never reach the user.active condition. To understand better, let's solve a simple exercise together. For this post, I've chosen a very busy code with several levels of nesting for you to work on:

main();


function main() { const transactions = [ { id: "t1", type: "PAYMENT", status: "OPEN", method: "CREDIT_CARD", amount: "23.99", }, { id: "t2", type: "PAYMENT", status: "OPEN", method: "PAYPAL", amount: "100.43", }, { id: "t3", type: "REFUND", status: "OPEN", method: "CREDIT_CARD", amount: "10.99", }, { id: "t4", type: "PAYMENT", status: "CLOSED", method: "PLAN", amount: "15.99", }, ];


processTransactions(transactions); }


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 provided!"); } }


function processCreditCardPayment(transaction) { console.log( "Processing credit card payment for amount: " + transaction.amount ); }


function processCreditCardRefund(transaction) { console.log( "Processing credit card refund for amount: " + transaction.amount ); }


function processPayPalPayment(transaction) { console.log("Processing PayPal payment for amount: " + transaction.amount); }


function processPayPalRefund(transaction) { console.log("Processing PayPal refund for amount: " + transaction.amount); }


function processPlanPayment(transaction) { console.log("Processing plan payment for amount: " + transaction.amount); }


function processPlanRefund(transaction) { console.log("Processing plan refund for amount: " + transaction.amount); }

We will work with this code several times in this post. If you look carefully, the if conditions are calling the methods defined in this file, but for now, focus on the big if condition in the processTransactions function. I want you to improve these nested bets by using guards. Note that even after using guards you will still have a small number of nested conditions.

The answer to this exercise is simple. In the first step, we must convert the first if condition in the processTransactions function into a guard. If you have read the code, you can assume that the transaction parameter is an array. With this account, we can reverse the condition as follows: there are no transactions or its length is equal to zero. Once you've done that, you can condition the contents of the else block, which is a console.log (by reversing the condition, the else block itself will be the condition). After doing this there is still one more part that can become a guard. Where? This part of the code:

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!");
}

We have a big condition here that has a lot of code inside it, and then we have an else condition for it that shows the error message. Anywhere in any program you see such code (a big if and an else to display an error), you should immediately realize that it is possible to use guards in it. The important issue here is that this condition is written in a for loop, and if we return, we will exit this loop by mistake, which is not a good thing. The solution to this problem is to use continue instead of return. Also, don't forget to put the console.log message before continue.

Now, for more convenience, I move the condition related to transaction.status !== 'Open' to the beginning of the conditions of this function. With a little correction and doing this operation, you get the following result:

main();


function main() { const transactions = [ { id: "t1", type: "PAYMENT", status: "OPEN", method: "CREDIT_CARD", amount: "23.99", }, { id: "t2", type: "PAYMENT", status: "OPEN", method: "PAYPAL", amount: "100.43", }, { id: "t3", type: "REFUND", status: "OPEN", method: "CREDIT_CARD", amount: "10.99", }, { id: "t4", type: "PAYMENT", status: "CLOSED", method: "PLAN", amount: "15.99", }, ];


processTransactions(transactions); }


function processTransactions(transactions) { if (!transactions || transactions.length === 0) { console.log("No transactions provided!"); return; }


for (const transaction of transactions) { if (transactions.status !== "OPEN") { console.log("Invalid transaction type!"); continue; } if (transaction.type === "PAYMENT") { if (transaction.method === "CREDIT_CARD") { processCreditCardPayment(transaction); } else if (transaction.method === "PAYPAL") { processPayPalPayment(transaction); } else if (transaction.method === "PLAN") { processPlanPayment(transaction); } } else if (transaction.type === "REFUND") { 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); } } }


function processCreditCardPayment(transaction) { console.log( "Processing credit card payment for amount: " + transaction.amount ); }


function processCreditCardRefund(transaction) { console.log( "Processing credit card refund for amount: " + transaction.amount ); }


function processPayPalPayment(transaction) { console.log("Processing PayPal payment for amount: " + transaction.amount); }


function processPayPalRefund(transaction) { console.log("Processing PayPal refund for amount: " + transaction.amount); }


function processPlanPayment(transaction) { console.log("Processing plan payment for amount: " + transaction.amount); }


function processPlanRefund(transaction) { console.log("Processing plan refund for amount: " + transaction.amount); }

In the future, we will work more on this code, but for now, we have been able to improve the readability of the code by using guards.

Extraction of control structures

As I explained in the previous post, one of the issues that cause the codes to be unreadable is the difference in the levels of the codes in the same block. In the previous post, we introduced breaking the code into different functions as a solution to deal with such problems, and this solution is also applicable in this section. For example, the first part of the processTransactions function is an if condition as follows:

if (!transactions || transactions.length === 0) {
    console.log("No transactions provided!");
    return;
  }

We can extract the two conditions that are checked in this if (transaction and its non-zero length (transaction.length)) in the form of another function. Doing this, in addition to shortening the if condition, allows us to use positive conditions (not using the ! sign in the condition), and as I said, the readability of positive conditions is more than the readability of negative conditions. With this account, we define a new function and say:

function isEmpty(transactions) {
  return !transactions || transactions.length === 0;
}

Now this function takes our transaction and checks our condition on it. To use it, you must call it inside the if condition:

function processTransactions(transactions) {
  if (isEmpty(transactions)) {
    console.log("No transactions provided!");
    return;
  }
// other codes

By doing this, we have made our code more readable, but the difference in the code levels in the if condition is still clear. We have a console.log statement that is more verbose than the rest of the code. Again, I say that changing this section is up to you and the needs of your programs. Usually, less than console.log is used in real programs, and this log statement is usually statements for error management. So I like to define it as a separate method so that if we want to edit it later, we don't need to edit 10 different parts of the program:

function showErrorMessage(message) {
  console.log(message);
}

Now we can use it in the if condition:

function processTransactions(transactions) {
  if (isEmpty(transactions)) {
    showErrorMessage("No transactions provided!");
    return;
  }

Next, I want to keep the for loop in its place because the name of the function is processTransactions, so it should be able to circulate between transactions, but inside this loop we have a big if condition that performs different operations based on the types of transactions. to give In my opinion, it is better to extract all this code and put it inside a completely new function. I'll name the new function processTransaction (without the plural s):

function processTransaction(transaction) {
  if (transactions.status !== "OPEN") {
    console.log("Invalid transaction type!");
    return;
  }
  if (transaction.type === "PAYMENT") {
    if (transaction.method === "CREDIT_CARD") {
      processCreditCardPayment(transaction);
    } else if (transaction.method === "PAYPAL") {
      processPayPalPayment(transaction);
    } else if (transaction.method === "PLAN") {
      processPlanPayment(transaction);
    }
  } else if (transaction.type === "REFUND") {
    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);
  }
}

Since we will receive the transaction as a parameter, the if condition inside it will still work and can access the type or method properties. Note that I've also separated our own guard (transaction.status !== 'OPEN) from the main loop and put it inside the processTransaction function. Of course, when we do this, there is no need to continue and we have returned it to the same return. Why? Because now the scope of the code is different. Previously, this guard was located directly inside a loop, but now it is located directly inside a function and is exited from the function with return.

With this account, we can go back to the processTransactions method (with a plus s) and call processTransaction inside it:

function processTransactions(transactions) {
  if (isEmpty(transactions)) {
    showErrorMessage("No transactions provided!");
    return;
  }


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

Now, as you can see, the processTransactions method is much shorter and more readable. We have kept the same for loop, but instead of writing the partial code line by line inside it, we have transferred the code to another independent function that takes one of the transactions in each round and processes it.

So that you can keep up with me, compare all your codes with all of mine. Your code should look exactly like this:

main();


function main() { const transactions = [ { id: "t1", type: "PAYMENT", status: "OPEN", method: "CREDIT_CARD", amount: "23.99", }, { id: "t2", type: "PAYMENT", status: "OPEN", method: "PAYPAL", amount: "100.43", }, { id: "t3", type: "REFUND", status: "OPEN", method: "CREDIT_CARD", amount: "10.99", }, { id: "t4", type: "PAYMENT", status: "CLOSED", method: "PLAN", amount: "15.99", }, ];


processTransactions(transactions); }


function processTransactions(transactions) { if (isEmpty(transactions)) { showErrorMessage("No transactions provided!"); return; }


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


function isEmpty(transactions) { return !transactions || transactions.length === 0; }


function showErrorMessage(message) { console.log(message); }


function processTransaction(transaction) { if (transactions.status !== "OPEN") { console.log("Invalid transaction type!"); return; } if (transaction.type === "PAYMENT") { if (transaction.method === "CREDIT_CARD") { processCreditCardPayment(transaction); } else if (transaction.method === "PAYPAL") { processPayPalPayment(transaction); } else if (transaction.method === "PLAN") { processPlanPayment(transaction); } } else if (transaction.type === "REFUND") { 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); } }


function processCreditCardPayment(transaction) { console.log( "Processing credit card payment for amount: " + transaction.amount ); }


function processCreditCardRefund(transaction) { console.log( "Processing credit card refund for amount: " + transaction.amount ); }


function processPayPalPayment(transaction) { console.log("Processing PayPal payment for amount: " + transaction.amount); }


function processPayPalRefund(transaction) { console.log("Processing PayPal refund for amount: " + transaction.amount); }


function processPlanPayment(transaction) { console.log("Processing plan payment for amount: " + transaction.amount); }


function processPlanRefund(transaction) { console.log("Processing plan refund for amount: " + transaction.amount); }

As you can see by looking at the code above, the processTransactions function is readable, but the nested if conditions that we transferred to the processTransaction function are still very crowded and unreadable. I want you to try to solve this problem yourself using the tips I explained in this post and then look at my answer.

If you look at the big if condition, you'll see the following two if and else clauses:

  • Payment section: This section is for paying money by customers to buy our goods or services.
  • Refund Section: This section is dedicated to returning customers' money in case of dissatisfaction with goods and services.

Accordingly, we can have two functions, each of which is responsible for processing one of these two types of transactions. I'll start with the first category and call this method processPayment:

function processPayment(paymentTransaction) {
  if (paymentTransaction.method === "CREDIT_CARD") {
    processCreditCardPayment(paymentTransaction);
  } else if (paymentTransaction.method === "PAYPAL") {
    processPayPalPayment(paymentTransaction);
  } else if (paymentTransaction.method === "PLAN") {
    processPlanPayment(paymentTransaction);
  }
}

As you can see, half of the big if condition related to Payments is placed inside this function. Since I want the received parameter of the function to be clear and explicit, I have changed its name from transaction to paymentTransaction. For this reason, you must change the name of the transaction to paymentTransaction in all this code.

In the next step, we do the same for the second half of the if condition. This time I have chosen the name processRefund for this function:

function processRefund(refundTransaction) {
  if (refundTransaction.method === "CREDIT_CARD") {
    processCreditCardRefund(refundTransaction);
  } else if (refundTransaction.method === "PAYPAL") {
    processPayPalRefund(refundTransaction);
  } else if (refundTransaction.method === "PLAN") {
    processPlanRefund(refundTransaction);
  }
}

Finally, we return to the processTransaction function (without s plus) and use these two functions in it:

function processTransaction(transaction) {
  if (transactions.status !== "OPEN") {
    console.log("Invalid transaction type!");
    return;
  }
  if (transaction.type === "PAYMENT") {
    processPayment(transaction);
  } else if (transaction.type === "REFUND") {
    processRefund(transaction);
  } else {
    console.log("Invalid transaction type!", transaction);
  }
}

By doing this, the code has become more readable, but there is still room for improvement. For example, we have two commands console.log in this function, which caused the level difference in the codes. One of these two commands, in addition to printing a message in the console, also prints the transaction itself and its data (the second argument for console.log), so if we want to use our own showErrorMessage function, we have to edit it a little:

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

This is a very simple way to do this. We print the error message first, and if a second argument (item) is sent, we also print it separately. Naturally, there are many ways to do this, and the above function is just one of several possible modes. Now we can use this function instead of console.logs:

function processTransaction(transaction) {
  if (transactions.status !== "OPEN") {
    showErrorMessage("Invalid transaction type!");
    return;
  }


if (transaction.type === "PAYMENT") { processPayment(transaction); } else if (transaction.type === "REFUND") { processRefund(transaction); } else { showErrorMessage("Invalid transaction type!", transaction); } }

I don't suggest breaking this code any further because almost everything you do from here on is overhead, but if you insist you can also specify the status of a transaction through another function. For example, I define the following three functions for three different states:

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


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


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

As you can see, these three functions are responsible for checking the type of transaction (Refund or Payment) and whether the transaction status is open. Now, using these three functions, we can rewrite the processTransaction function as follows:

function processTransaction(transaction) {
  if (!isOpen(transaction)) {
    showErrorMessage("Invalid transaction type!");
    return;
  }
  if (isPayment(transaction)) {
    processPayment(transaction);
  } else if (isRefund(transaction)) {
    processRefund(transaction);
  } else {
    showErrorMessage("Invalid transaction type!", transaction);
  }
}

In my opinion, this type of writing is also acceptable and readable, although we may have gone too far in the opinion of many people.

[hive: @albro]



0
0
0.000
2 comments
avatar

Thanks for your contribution to the STEMsocial community. Feel free to join us on discord to get to know the rest of us!

Please consider delegating to the @stemsocial account (85% of the curation rewards are returned).

You may also include @stemsocial as a beneficiary of the rewards of this post to get a stronger support. 
 

0
0
0.000