I believe many students have encountered business logic entangled between deep if else when maintaining old projects. In the face of such a mess, simply adding incremental changes often makes it more complex and less readable. Is there any set formula for sorting it out? Here are three simple and common ways to refactor.

What is noodle code

The so-called noodle code is common in complex business processes. It generally meets the following characteristics:

  • The content length
  • The structure random
  • Nested deep

As we know, all major programming languages have functions or methods to organize code. For noodle code, think of it as a function that satisfies these characteristics. According to the differences in language semantics, it can be divided into two basic types:

if... if

This type of code is structured like:

function demo (a, b, c) {
  if (f(a, b, c)) {
    if (g(a, b, c)) {
      // ...
    }
    // ...
    if (h(a, b, c)) {
      // ...}}if (j(a, b, c)) {
    // ...
  }

  if (k(a, b, c)) {
    // ...}}Copy the code

The process graph is as follows:

if-if-before

It allows the flow of control within a single function to grow by nesting ifs from top to bottom. Don’t assume that complexity increases linearly as the control flow grows. We know that functions deal with data, and every IF usually has logic to deal with that data. Then, even if there is no nesting, if there are three such ifs, then there are 2 ^ 3 = 8 states of data, depending on whether each IF is executed. If there are 6, then there are 2 ^ 6 = 64 states. This makes debugging functions exponentially harder as the project scales up! This is in order of magnitude consistent with the experience of the Man-month Myth.

else if... else if

This type of code control flow is also very common. Like:

function demo (a, b, c) {
  if (f(a, b, c)) {
    if (g(a, b, c)) {
      // ...
    }
    // ...
    else if (h(a, b, c)) {
      // ...
    }
    // ...
  } else if (j(a, b, c)) {
    // ...
  } else if (k(a, b, c)) {
    // ...}}Copy the code

The process graph is as follows:

else-if-before

Else if’s are only going to end up in one of these branches, so you don’t have the combination explosion above. However, the complexity is also not low at deep nesting. Suppose there are 3 else if’s on each of the 3 layers, then there are 3 ^ 3 = 27 exits. If each exit corresponds to one way of handling data, then wrapping so much logic in one function would clearly violate the single responsibility principle. Moreover, the two types can be combined seamlessly, further increasing complexity and reducing readability.

But why, in this era of advanced frameworks and libraries, does this code still appear so often? My personal opinion is that reuse modules do allow us to write less [template code], but the business itself, no matter how encapsulated, needs the developer to write the logic. And even a simple if else can make the control flow exponentially more complex. From this point of view, without basic programming literacy, no matter how good the quick mastery of frameworks and libraries can be, the project will be a mess.

Refactoring strategy

Above, we have discussed two types of noodle code and quantitatively demonstrated how they can cause control flow complexity to soar exponentially. In modern programming languages, however, this complexity is completely manageable. The following is a list of programming techniques for improving noodle code in several cases.

The basic situation

For the seemingly fastest-growing if… If noodle code, which can be broken down by basic functions. In the figure below, each green box represents a new function that is split:

if-if-after

Since modern programming languages have abandoned goto, no matter how complex the control flow, the code inside a function is executed from the top down. Therefore, we have the ability to divide a single large function into several small functions and then call them one by one without changing the control flow logic. This is a technique often used by experienced students, and the specific code implementation will not be described here.

It is important to note that by not changing the control flow logic, this means that the change does not need to change the way the business logic is executed, but simply [move the code out and wrap it in functions]. Some students may think that this method is just a temporary solution, but not the root cause, just a large section of noodles cut into several smaller pieces, there is no essential difference.

But is it really so? In this way, we can break up a large function with 64 states into six smaller functions that return only two different states, and a main function that calls them one by one. In this way, the rate of increase in complexity of each function is reduced from exponential to linear.

In this way, we have solved the problem of if… If type noodle code, so else if… What about else if?

A lookup table

For else if… One of the simplest refactoring strategies for noodle code of type else if is to use what’s called a lookup table. It encapsulates the logic in each else if in the form of key-value pairs:

const rules = {
  x: function (a, b, c) { / *... * / },
  y: function (a, b, c) { / *... * / },
  z: function (a, b, c) { / *... * /}}function demo (a, b, c) {
  const action = determineAction(a, b, c)
  return rules[action](a, b, c)
}Copy the code

With the logic in each else if rewritten to a separate function, we can break up the process as follows:

else-if-lookup

This is a trivial technique for scripting languages that inherently support reflection. But for more complex else if conditions, this approach reconcentrates the complexity of the control flow into the determineAction that handles the question “Which branch to take”. Is there a better way to handle it?

Chain of Responsibility model

In the previous case, the lookup table was implemented as a key-value pair, and ‘foo’ can be used as the key of the refactoring set in a case where each branch is a simple judgment like else if (x === ‘foo’). But if each else if branch contains complex conditional decisions that require the order of execution, then we can better refactor such logic with the chain of responsibility pattern.

For the else if, notice that each branch is actually judged from the top down, and only one of them goes in. This means that we can implement this behavior by storing an array of decision rules. If the rule matches, then the branch corresponding to the rule is executed. We call such arrays chains of responsibility, and the execution process in this mode is as follows:

else-if-chain

In the code implementation, we can define the exact equivalent of the else if rule with an array of chains of responsibilities:

const rules = [
  {
    match: function (a, b, c) { / *... * / },
    action: function (a, b, c) { / *... * /}}, {match: function (a, b, c) { / *... * / },
    action: function (a, b, c) { / *... * /}}, {match: function (a, b, c) { / *... * / },
    action: function (a, b, c) { / *... * /}}// ...
]Copy the code

Each entry in rules has a match and action attribute. We can override the function else if by iterating over the chain of responsibilities array:

function demo (a, b, c) {
  for (let i = 0; i < rules.length; i++) {
    if (rules[i].match(a, b, c)) {
      return rules[i].action(a, b, c)
    }
  }
}Copy the code

In this case, once each responsibility is matched, the original function will return directly, which is exactly the same as else if semantics. In this way, we have achieved the separation of the monomer complex else if logic.

conclusion

Noodle code is prone to unthinking “rough and fast” development. A lot of simple [if here, multiple returns there] bug fixes, coupled with a lack of comments, can easily make code less readable and more complex.

But none of the solutions to the problem is complicated. The simplicity of these examples is essentially due to the expressive power of high-level programming languages that have enabled them to provide direct semantic support for requirements without relying on template code from various design patterns.

Sure, you can use patterns to generalize techniques for reducing the complexity of your business logic, but memorizing and using patterns can also lead to overdesign. When realizing common business functions, mastering programming languages, sorting out requirements and implementing them with the simplest code is the optimal solution.