Solve the problem of too many if ... else in the code

Foreword

if ... else is a must-have feature for all high-level programming languages. But the code in reality often has too many if ... else. Although if ... else is necessary, misuse of if ... else will cause great harm to the readability and maintainability of the code, and then endanger the entire software system. Now many new technologies and new concepts have appeared in the field of software development, but the basic program form of if ... else has not changed much. Using good if ... else is very meaningful not only for the present, but also for the future. Today we will take a look at how to "kill" if ... else in the code, and return the code to be refreshing.

Question 1: too many if ... else

Problem performance

 

If ... else too much code can be abstracted as the following code. Only 5 logical branches are listed, but in actual work, we can see that a method contains 10, 20 or more logical branches. In addition, too many if ... else will usually be accompanied by two other problems: complex logical expressions and if ... else are too deeply nested. For the latter two questions, this article will be introduced in the following two sections. This section first discusses the situation of too many if ... else.

 

if (condition1) {

} else if (condition2) {

} else if (condition3) {

} else if (condition4) {

} else {

}

 

Usually, there are too many if ... else methods, and the readability and scalability are usually not good. From a software design perspective, there are too many if ... else in the code often means that this code violates the principle of single responsibility and the principle of opening and closing. Because in actual projects, the demand is constantly changing, and new demands are also emerging. Therefore, the scalability of the software system is very important. The biggest meaning of solving if ... else too many problems is often to improve the scalability of the code.

 

How to solve

 

Next we look at how to solve the problem of too many if ... else. Below I have listed some solutions.

 

  1. Table driven

  2. Chain of responsibility

  3. Annotation-driven

  4. Event-driven

  5. Finite State Machine

  6. Optional

  7. Assert

  8. Polymorphism

 

Method One: Table Drive

 

Introduction

 

For if ... else code with a fixed logical expression mode, you can express the logical expression in a table through a certain mapping relationship; then use the table search method to find the processing function corresponding to an input, use this processing Function for operation.

 

Applicable scene

Logical expression mode fixed if ... else

 

Implementation and examples

 

if (param.equals(value1)) {
    doAction1(someParams);
} else if (param.equals(value2)) {
    doAction2(someParams);
} else if (param.equals(value3)) {
    doAction3(someParams);
}
// ...

 

Reconfigurable as

 

Map<?, Function<?> action> actionMappings = new HashMap<>(); // 这里泛型 ? 是为方便演示,实际可替换为你需要的类型

// When init
actionMappings.put(value1, (someParams) -> { doAction1(someParams)});
actionMappings.put(value2, (someParams) -> { doAction2(someParams)});
actionMappings.put(value3, (someParams) -> { doAction3(someParams)});

// 省略 null 判断
actionMappings.get(param).apply(someParams);

 

The above example uses Java 8 Lambda and Functional Interface, which will not be explained here.

The mapping relationship between tables can be centralized or decentralized, that is, each processing class registers itself. It can also be expressed in the form of configuration files. In short, there are many forms.

 

There are still some problems. The conditional expression is not as simple as in the above example, but with a little transformation, the table drive can also be applied. The following borrows a tax calculation example from "Programming Pearl":

 

if income <= 2200
  tax = 0
else if income <= 2700
  tax = 0.14 * (income - 2200)
else if income <= 3200
  tax = 70 + 0.15 * (income - 2700)
else if income <= 3700
  tax = 145 + 0.16 * (income - 3200)
......
else
  tax = 53090 + 0.7 * (income - 102200)

 

For the above code, in fact, just extract the tax calculation formula, extract the standard of each file into a table, and add a cycle. The code after the specific refactoring is not given, everyone thinks for themselves.

 

Method 2: Chain of responsibility model

 

Introduction

 

When the conditional expression in if ... else is flexible and cannot be abstracted into a table and judged in a unified manner, the judgment of the condition should be handed over to each functional component. In the form of chain, these components are connected in series to form a complete function.

 

Applicable scene

 

Conditional expressions are flexible and there is no unified form.

 

Implementation and examples

 

The mode of responsibility chain can be seen in the realization of the Filter and Interceptor functions of the open source framework. Let's take a look at the general usage mode:

 

Before refactoring:

 

public void handle(request) {
    if (handlerA.canHandle(request)) {
        handlerA.handleRequest(request);
    } else if (handlerB.canHandle(request)) {
        handlerB.handleRequest(request);
    } else if (handlerC.canHandle(request)) {
        handlerC.handleRequest(request);
    }
}
 

After refactoring:

 

public void handle(request) {
  handlerA.handleRequest(request);
}

public abstract class Handler {
  protected Handler next;
  public abstract void handleRequest(Request request);
  public void setNext(Handler next) { this.next = next; }
}

public class HandlerA extends Handler {
  public void handleRequest(Request request) {
    if (canHandle(request)) doHandle(request);
    else if (next != null) next.handleRequest(request);
  }
}
 

Of course, the code before refactoring in the example did some extraction and refactoring of classes and methods in order to express clearly. In reality, it is more tiled code implementation.

 

Note: The control mode of the chain of responsibility

 

The chain of responsibility model will have some different forms in the specific implementation process. From the perspective of chain call control, it can be divided into external control and internal control.

 

External control is not flexible, but reduces the difficulty of implementation. The specific implementation on one ring of the responsibility chain does not need to consider the call to the next ring, because the external unified control. But the general external control can not realize nested call. If there are nested calls and you want to control the call of the responsibility chain from outside, it will be a little more complicated to implement. For details, please refer to the implementation method of Spring Web Interceptor mechanism.

 

Internal control is more flexible, and the specific implementation can decide whether to call the next ring in the chain. But if the call control mode is fixed, such an implementation is inconvenient for the user.

 

There are many variations of design patterns in specific use, everyone needs to be flexible

 

Method 3: Annotation-driven

 

Introduction

 

Java annotations (or similar mechanisms in other languages) define the conditions for executing a method. When the program is executed, it is decided whether to call this method by comparing whether the conditions defined in the participating comments match. In specific implementation, it can be implemented in the form of table-driven or chain of responsibility.

 

Applicable scene

 

It is suitable for scenarios with many conditional branches and high requirements for program scalability and ease of use. It is usually a core function that often encounters new requirements in a system.

 

Implementation and examples

 

The use of this pattern can be seen in many frameworks, such as the common Spring MVC. Because these frameworks are very commonly used and demos can be seen everywhere, there is no more specific demo code here.

 

The focus of this model is implementation. Existing frameworks are used to implement functions in a specific field, such as MVC. Therefore, if the business system adopts this mode, it needs to implement related core functions by itself. It will mainly involve technologies such as reflection and chain of responsibility. The specific implementation will not be demonstrated here.

 

Method 4: Event-driven

 

Introduction

 

By associating different event types and corresponding processing mechanisms, complex logic is achieved, while achieving the goal of decoupling.

 

Applicable scene

 

From a theoretical point of view, event-driven can be regarded as a kind of table-driven, but from a practical point of view, event-driven and table-driven mentioned above are different in many ways. Specifically:

 

  1. Table-driven is usually a one-to-one relationship; event-driven is usually one-to-many;

  2. In table-driven, triggering and execution are usually strongly dependent; in event-driven, triggering and execution are weakly dependent

 

It is the difference between the above two that leads to the difference in the applicable scenarios of the two. Specifically, event-driven can be used to trigger inventory, logistics, points and other functions such as order payment completion.

 

Implementation and examples

 

In terms of implementation, the stand-alone practice drive can be implemented using frameworks such as Guava and Spring. Distributed is generally achieved through various message queues. But because the main discussion here is to eliminate if ... else, it is mainly oriented to the single machine problem domain. Because of the specific technology involved, this mode code is not demonstrated.

 

Method 5: Finite state machine

 

Introduction

 

Finite state machines are often called state machines (the concept of infinite state machines can be ignored). First quote the definition on Wikipedia:

The finite state machine (English: finite-state machine, abbreviation: FSM), referred to as the state machine, is a mathematical model that represents a finite number of states and the behavior of transitions and actions between these states.

In fact, the state machine can also be regarded as a kind of table driven, which is actually a correspondence between the combination of the current state and the event and the processing function. Of course, there will be a state transition process after the process is successful.

 

Applicable scene

 

Although Internet back-end services are now emphasizing statelessness, this does not mean that the state machine design cannot be used. In fact, in many scenarios, such as protocol stack, order processing and other functions, the state machine has its natural advantages. Because these situations naturally exist in the state and state circulation.

 

Implementation and examples

 

To implement state machine design, you need to have a corresponding framework. This framework needs to implement at least one state machine definition function and the corresponding call routing function. The state machine definition can use DSL or annotations. The principle is not complicated, and students who have mastered functions such as annotation and reflection should be easy to implement.

 

Reference technology:

  • Apache Mina State Machine
    Apache Mina framework, although not as good as Netty in the field of IO framework, but it provides a state machine function. https://mina.apache.org/mina-project/userguide/ch14-state-machine/ch14-state-machine.html. Students who have their own state machine functions can refer to their source code.


  • There are many Spring State Machine Spring sub-projects, among which there is a state machine framework that does not show mountains and water—Spring State Machine https://projects.spring.io/spring-statemachine/. It can be defined in two ways: DSL and annotation.

 

The above framework only serves as a reference. If specific projects are involved, the core functions of the state machine need to be implemented based on business characteristics.

 

Method 6: Optional

Introduction

 

A part of if ... else in the Java code is caused by a non-null check. Therefore, reducing the if ... else brought by this part can also reduce the overall number of if ... else.

 

Java introduced the Optional class from 8 to represent objects that may be empty. This class provides many methods for related operations, which can be used to eliminate if ... else. The open source frameworks Guava and Scala languages ​​also provide similar functionality.

 

scenes to be used

 

There are more if ... else for non-empty judgment.

 

Implementation and examples

 

Traditional writing:

 

String str = "Hello World!";
if (str != null) {
    System.out.println(str);
} else {
    System.out.println("Null");
}

 

After using Optional:

 

1 Optional<String> strOptional = Optional.of("Hello World!");
2 strOptional.ifPresentOrElse(System.out::println, () -> System.out.println("Null"));

 

There are many methods for Optional, which are not introduced here one by one. But please note, don't use get () and isPresent () methods, otherwise it is the same as traditional if ... else.

 

Extension: Kotlin Null Safety

 

Kotlin comes with a feature called Null Safety:

bob?.department?.head?.name

 

For a chained call, in Kotlin language, you can pass?. To avoid null pointer exceptions. If a ring is null, the value of the entire chain expression is null.

 

Method 7: Assert mode

 

Introduction

 

The last method is suitable for solving if ... else caused by non-empty inspection scenarios. Similar scenarios have various parameter verifications, such as the string is not empty and so on. Many framework libraries, such as Spring and Apache Commons, provide tools for implementing this common function. So you do n’t have to write if ... else yourself.

  • Validate class in Apache Commons Lang: https://commons.apache.org/proper/commons-lang/javadocs/api-3.1/org/apache/commons/lang3/Validate.html

  • Spring's Assert class: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/util/Assert.html

 

scenes to be used

 

Usually used for various parameter verification

 

Extension: Bean Validation

 

Similar to the previous method, introduce the Assert pattern and introduce a technology with a similar effect-Bean Validation. Bean Validation is one of the Java EE specifications. Bean Validation uses annotations to define validation standards on Java Beans, and then validates them uniformly through the framework. It can also play a role in reducing if ... else.

 

Method 8: Polymorphism

Introduction

 

Using object-oriented polymorphism can also eliminate if ... else. In the book of code refactoring, this is also introduced:

https://refactoring.com/catalog/replaceConditionalWithPolymorphism.html

 

scenes to be used

 

The example given in the link is relatively simple and cannot reflect a specific scenario suitable for polymorphic elimination if ... else. In general, when multiple methods in a class have similar if ... else judgments in the example, and the conditions are the same, then you can consider using polymorphism to eliminate if ... else.

 

At the same time, using polymorphism does not eliminate if ... else completely. Instead, the if ... else merge was transferred to the object creation stage. In the creation stage of if .., we can use the method described earlier.

 

summary

 

The above section introduces the problems caused by too many if ... else and the corresponding solutions. In addition to the methods described in this section, there are other methods. For example, in the book "Refactoring and Patterns", three methods are introduced: "replace conditional logic with Strategy", "replace state change conditional statement with State" and "replace conditional scheduler with Command". The "Command mode" in it is roughly the same as the "table-driven" approach in this article. The other two methods, because they have been explained in detail in the book "Refactoring and Patterns", will not be repeated here.

 

When to use which method depends on the type of problem you are facing. Some of the applicable scenarios described above are just suggestions, and more need to be considered by developers themselves.

Question 2: if ... else is too deeply nested

Problem performance

 

If ... else is usually not the most serious problem. Some code if ... else is not only large in number, but also nested between if ... else is very deep and complex, resulting in poor code readability and naturally difficult to maintain.

 

if (condition1) {
    action1();
    if (condition2) {
        action2();
        if (condition3) {
            action3();
            if (condition4) {
                action4();
            }
        }
    }
}

 

If ... else is too deeply nested, it will seriously affect the readability of the code. Of course, there will be two problems mentioned in the previous section.

 

How to solve

 

The method introduced in the previous section can also be used to solve the problems in this section, so for the above method, this section will not be repeated. This section focuses on some methods that will not reduce the number of if ... else, but will improve the readability of the code:

 

  1. Extraction method

  2. Wei statement

 

Method 1: Extraction method

 

Introduction

 

The extraction method is a means of code reconstruction. The definition is easy to understand, that is, to extract a piece of code and put it into another separately defined method. borrow

Use the definition in https://refactoring.com/catalog/extractMethod.html:

 

Applicable scene

if ... else is heavily nested code, which is often poorly readable. Therefore, before large-scale refactoring, minor adjustments are needed to improve its code readability. The extraction method is the most commonly used adjustment method.

 

Implementation and examples

 

Before refactoring:

 

public void add(Object element) {
  if (!readOnly) {
    int newSize = size + 1;
    if (newSize > elements.length) {
      Object[] newElements = new Object[elements.length + 10];
      for (int i = 0; i < size; i++) {
        newElements[i] = elements[i];
      }

      elements = newElements
    }
    elements[size++] = element;
  }
}

 

After refactoring:

 

public void add(Object element) {
  if (readOnly) {
    return;
  }

  if (overCapacity()) {
    grow();
  }

  addElement(element);
}

 

Method 2: Wei statement

 

Introduction

 

In code refactoring, there is a method called "use guard statement instead of nested conditional statement" https://refactoring.com/catalog/replaceNestedConditionalWithGuardClauses.html. Look directly at the code:

 

double getPayAmount() {
    double result;
    if (_isDead) result = deadAmount();
    else {
        if (_isSeparated) result = separatedAmount();
        else {
            if (_isRetired) result = retiredAmount();
            else result = normalPayAmount();
        };
    }
    return result;
}

 

After refactoring

 

double getPayAmount() {
    if (_isDead) return deadAmount();
    if (_isSeparated) return separatedAmount();
    if (_isRetired) return retiredAmount();
    return normalPayAmount();
}

 

scenes to be used

 

When you see a method where a certain block of code is completely controlled by an if ... else, you can usually use a guard statement.

Question 3: if ... else expression is too complicated

Problem performance

 

The third problem caused by if ... else comes from overly complicated conditional expressions. Here is a simple example. When condition 1, 2, 3, and 4 are true and false, please arrange and combine the results of the following expressions.

 

1 if ((condition1 && condition2 ) || ((condition2 || condition3) && condition4)) {
2   
3 }
 

I think no one wants to do the above. The key is, what is the meaning of this big Tuo expression? The point is that when the meaning of an expression is not known, no one wants to infer its result.

 

Therefore, the expression is complicated and not necessarily wrong. But expressions are not easy to understand.

 

How to solve

 

For the complicated expression of if ... else expression, it is mainly solved by means of extraction and movement in code reconstruction. Because these methods are introduced in the book "Code Refactoring", they will not be repeated here.

to sum up

This article introduces 10 methods (including 12 expansions) for eliminating and simplifying if ... else. There are also some methods, such as eliminating if through strategy mode, state mode, etc ... else is also introduced in the book "Refactoring and Mode".

 

As mentioned in the preface, if ... else is an important part of the code, but excessive and unnecessary use of if ... else will negatively affect the readability and scalability of the code, which will affect the entire Software system.

 

The ability to "kill" if ... else reflects the programmer's comprehensive ability to use software refactoring, design patterns, object-oriented design, architectural patterns, data structures and other technologies, and reflects the programmer's internal work. If you want to use if ... else reasonably, you can't do it without design or over-design. The comprehensive and reasonable use of these technologies requires programmers to constantly explore and summarize in their work.

Published 117 original articles · 69 praises · 10,000+ views

Guess you like

Origin blog.csdn.net/zsd0819qwq/article/details/105321890