How to optimize if there are too many if statements in Java code of series 17?

Posted by LankyMac on Fri, 12 Nov 2021 02:22:30 +0100

1, Today's topic

During our interview, the interviewer will examine whether we have participated in the project from all aspects, not only our mastery and understanding of a certain skill, but also our programming habits and skills. For example, there is an interview question:

If there are a lot of if/else statements in your project code, what optimization solution do you have?

2, Topic analysis

In the code we usually develop and write, if else judgment statements are basically essential. It's OK when we only have one or two layers to judge the nesting of statements, but when we use if...else statements excessively and unnecessarily, it will have a negative impact on the readability and scalability of the code. In addition, if there are more and more judgment statements, it will be difficult to maintain the project in the later stage, which is also a headache for those who take over the project later.

Therefore, removing too many if...else statements in the code reflects the comprehensive application ability of programmers' software reconstruction, design pattern, object-oriented design, architecture pattern, data structure and other technologies. Therefore, if...else should be used reasonably in our code, neither without nor excessive. The comprehensive and rational use of a certain technology requires our programmers to constantly explore and summarize in their work.

This is also the purpose of this interview question!

3, Existing problems

1. Example code

First, brother Yi will show you the following code. Please recall that in your previous project, there was no code of the following style. In a class or method, there were a large number of if...else if...else.

if (condition1) {
    doSomeThing1();
} else if (condition2) {
    doSomeThing2();
} else if (condition3) {
    doSomeThing3(); 
} else if (condition4) {
    doSomeThing4();
} else {
    doSomeThing5(); 
}...

Such code is the stacking of if and else. Of course, the function can be realized, that is, the code always looks strange. It is said that some complex businesses in a large factory are nested like this. For the later project maintenance and transformation, such code looks like a headache and disgusting "sea of shit".

2. Causes of problems

So why does this code appear? In fact, there are many reasons for this, such as

  • The design is not perfect;
  • Imperfect demand consideration;
  • Changes in developers, etc.

In fact, it is basically that the previous iteration is too lazy to optimize. For one demand, add an if. For another demand, add another if. Over time, it has become an overlapping if...else.

4, Solution

So how can we solve the excessive if...else statements in the code? The following solutions can be considered.

  1. Timely return;
  2. Replace with switch or ternary operator;
  3. Strategy mode: polymorphism, functional programming, enumeration;
  4. Use Optional;
  5. Use Assert;
  6. Table driven mode;
  7. Responsibility chain model;
  8. Annotation driven;
  9. Event driven;
  10. Finite state machine

1. Timely return

For example, we have the following code:

if (condition) {
	// do something
} else {   
	return xxx;
}

In fact, the above code can be optimized as follows:

if (!condition) {
	return xxx;
}

// do something

Let's judge first! condition, put the return statement in front, so you can remove the else statement. Compared with the above statement, the code is much clearer.

2. Replace with switch or ternary operator

For if statements that meet specific conditions, you can consider selecting switch or ternary operator for replacement, which will not be described in detail here.

3. Strategy mode

3.1 strategic model concept

As a software design pattern, policy pattern means that an object has a certain behavior, but in different scenarios, the behavior has different implementation algorithms. In strategy pattern, the behavior of a class or its algorithm can be changed at run time. This type of design pattern belongs to behavioral pattern. The policy mode consists of the following three roles:

  1. Abstract strategy: policy class, usually implemented by an interface or abstract class.
  2. Concrete strategy: encapsulates relevant algorithms and behaviors.
  3. Context: holds a reference to a policy class, which is finally called to the client.

3.2 policy mode usage scenarios

  1. There are many ways to deal with the same type of problem, only when the specific methods are different.
  2. When the same abstract class has multiple subclasses and you need to use if else or switch case to select specific subclasses.

3.3 specific cases of strategic mode

For example, there is a development scenario where we need to perform different operations according to different genders. Similar scenarios are very common. The general implementation is as follows:

if ("man".equals(strategy)) {   
	// Perform related operations 
} else if ("woman".equals(strategy)) {   
	// Perform operations related to women
} else if ("other".equals(strategy)) {   
	// Perform other operations
}

For the above code, we can adopt the following strategies for optimization.

3.3.1 optimization mode with polymorphic strategy

First, define an interface class Strategy.

public interface Strategy {

    void run() throws Exception;

}

Then write several implementation classes:

//Men's strategy implementation class
@Slf4j
public class ManStrategy implements Strategy {

    @Override
    public void run() throws Exception {
        // Fast man's logic
        log.debug("Execute the logic related to men...");
    }

}

//Women's strategy implementation class
@Slf4j
public class WomanStrategy implements Strategy {

    @Override
    public void run() throws Exception {
        // Fast woman's logic
        log.debug("Execute women related logic...");
    }

}

//Others' policy implementation class
@Slf4j
public class OtherStrategy implements Strategy {

    @Override
    public void run() throws Exception {
        // Fast other logic
        log.debug("Perform other related logic...");
    }

}

Simple examples:

public class StrategyTest {

    public static void main(String[] args) {
        try {
            Strategy strategy = initMap("man");
            strategy.run();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //Initialize the Map to obtain a gender policy
    private static Strategy initMap(String key) {
        //Use simple example
        HashMap<String, Strategy> map = new HashMap<>();
        map.put("man", new ManStrategy());
        map.put("woman", new WomanStrategy());
        map.put("other", new OtherStrategy());
        return map.get(key);
    }

}

Disadvantages of this scheme:

This optimization scheme has a disadvantage: in order to quickly get the corresponding implementation strategy, a map object is needed to save the implementation strategy. When adding a new strategy, it also needs to be manually added to the map, which is easy to be ignored.

3.3.2 strategy optimization mode of functional programming

We can make some modifications to the above code and use the functional programming method in Java 8 to simplify the code.

First define an interface.

public interface Strategy {

    void run() throws Exception;
}

Then use functional programming to operate the code.

@Slf4j
public class StrategyTest {

    public static void main(String[] args) {
        //Use simple example
        HashMap<String, Strategy> map = new HashMap<>();
        map.put("man", () -> log.debug("Execute the logic related to men..."));
        map.put("woman", () -> log.debug("Execute women related logic..."));
        map.put("other", () -> log.debug("Execute logic related to others..."));

        try {
            map.get("woman").run();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

3.3.3 enumeration based strategy optimization mode

Define an enumeration policy class.

@Slf4j
public enum Strategy {

    //Man state
    MAN(0) {
        @Override
        void run() {
            //Perform related operations
            log.debug("Execute the logic related to men");
        }
    },
    //Woman state
    WOMAN(1) {
        @Override
        void run() {
            //Perform operations related to women
            log.debug("Execute women related logic");
        }
    },
    //Other status
    OTHER(2) {
        @Override
        void run() {
            //Perform other related operations
            log.debug("Perform other related logic");
        }
    };

    abstract void run();

    public int statusCode;

    Strategy(int statusCode) {
        this.statusCode = statusCode;
    }

}

Simple use example:

public static void main(String[] args) {
        try {
            //Simple use example
            String param = String.valueOf(Strategy.WOMAN);
            Strategy strategy = Strategy.valueOf(param);
            strategy.run();
        } catch (Exception e) {
            e.printStackTrace();
        }
}

3.4 advantages of strategic mode

  1. The Strategy class (or interface, etc.) of the policy pattern defines a series of reusable algorithms or behaviors for Context. Inheritance helps to analyze the common functions in these algorithms.
  2. Using policy mode can avoid using multiple conditional transition statements. Multiple transfer statements are not easy to maintain. It mixes the logic of which algorithm or behavior to adopt with the logic of algorithm or behavior. They are all listed in a multiple transfer statement, which is primitive and backward than the method of inheritance.
  3. Simplifies unit testing. Because each algorithm has its own specific implementation class, it can be tested separately through its own interface.

3.5 disadvantages of strategic mode

  1. The client must know all policy classes and decide which policy class to use. This means that the client must understand the differences between these algorithms in order to select the appropriate algorithm class in time. In other words, the policy pattern is only applicable when the client knows all the algorithms or behaviors.
  2. The policy pattern creates many policy classes, and each specific policy class will generate a new class.

4. Optional

Some if...else in Java code is caused by non null checking. Therefore, reducing the if...else brought by this part can also reduce the number of if...else as a whole.

Java has introduced the Optional class since 8 to represent objects that may be empty. This class provides many methods for related operations and can be used to eliminate if...else. The open source framework Guava and Scala also provide similar functions. This method is applicable to if...else statements with more non null judgments.

Example code:

String str = "Hello World!";

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

With Optional Optimization:

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

5. Assert mode

Assert is used like a kind of "contractual programming". As the name suggests, if the program does not meet a specific condition or the input does not comply with a convention, the program will terminate execution. When dealing with exceptions, we usually use if for logic processing to achieve the robustness of the program. In the face of abnormal situations, if's approach is more gentle, while assert is simple and rough. However, the use of if statement will cause great burden, as shown below:

if(Hypothesis established){
    The program runs normally;
}else{
    report errors&&Terminate the program! (avoid larger errors caused by program operation)  
}

After all, the occurrence of exceptions is a few cases. If you use if statements everywhere for judgment and processing, there will be N more if statements, and even the parentheses of an IF statement will appear from the file head to the file end. In most cases, the occurrence of an accident is only an accidental event, so here is the assert assertion, as shown below:

//If the result of assertion 1 is true, continue to execute
assert true;
System.out.println("Assertion 1 has no problem, Let`s Go!");

try{
    //The result of assertion 2 is false, and the program terminates
    assert false : "If the assertion fails, the information of this expression will be output when an exception is thrown!";
    System.out.println("There was a problem with assertion 2, Stop the World!");
}catch (AssertionError err){
    System.out.println(err.getMessage());
}

6. Table drive mode

For if...else code with fixed logical expression mode, the logical expression can be expressed in a table through some mapping relationship, that is, query information from the table to find the processing logic function corresponding to an input, and use this processing function for operation. This method is applicable to if...else statements with fixed logical expression mode.

For example, the following code:

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

The Lambda and Functional Interface of Java 8 can be used for refactoring.

//Generic here? For the convenience of demonstration, it can be replaced with the real type we need in actual development
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)});
 
// Omit null judgment
actionMappings.get(param).apply(someParams);

7. Responsibility chain model

When the conditional expression in if...else is flexible and cannot abstract the data in the condition into a table and judge in a unified way, you can hand over the judgment right of the condition to each functional component, and connect these components in series in the form of method chain to form a complete function. This method is suitable for conditional expressions, which are flexible and have no unified form.

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

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

Refactoring Code:

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

8. Annotation driven

Conditions for executing a method are defined through Java annotations (or similar mechanisms in other languages). When the program is executed, it is determined whether to call this method by comparing whether the conditions defined in the input participation annotation match. The specific implementation can be implemented in the form of table driven or responsibility chain. It is suitable for scenarios with many conditional branches and high requirements for program scalability and ease of use. It is usually the core function of a system that often meets new requirements.

9. Event driven

By associating different event types and corresponding processing mechanisms, complex logic is realized and decoupled at the same time. 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 is different from the table driven mentioned earlier. Specifically:

  1. Table driven is usually a one-to-one relationship, and 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 different applicable scenarios. Specifically, event driven can be used to trigger inventory, logistics, points and other functions such as order payment completion.

10. Finite state machine

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

Finite state machine (English: finite state machine, abbreviated as FSM), abbreviated as state machine, is a mathematical model representing finite states, transitions and actions between these states * *.

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

Although the Internet back-end services are emphasizing statelessness, this does not mean that the design of state machine 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 there are natural states and state flows in these scenes.

5, Summary

This topic examines our programming habits and thinking. It requires us not only to complete the functions, but also to ensure the beauty of the code style. As long as we think more about the code details during development, these goals are easy to achieve. Finally, I'll give you a summary of how to simplify a large number of if statements in the code.

  1. Timely return;
  2. Replace with switch or ternary operator;
  3. Strategy mode: polymorphism, functional programming, enumeration;
  4. Use Optional;
  5. Assert;
  6. Table driven mode;
  7. Responsibility chain model;
  8. Annotation driven;
  9. Event driven;
  10. Finite state machine

Please choose one or more strategies reasonably according to the specific needs of the project.

Topics: Java Back-end