There are a lot of if/else in the code, what optimization plan do you have?

Viewpoint 1 (Spiritual Sword):

I was too lazy to optimize in the previous iterations, so I added an if to a requirement, and over time, it became a pyramid.

When the code is too complicated to maintain, it can only be refactored and optimized. So, is there any solution to elegantly optimize these redundant if/else?

1. Early return

This is the method of inverting the judgment condition, and the code will be clearer in logical expression, see the following code:

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

In fact, every time I see the above code, I scratch my heart. I can judge the !condition first and get rid of the else.

if (!condition) {
  return xxx;

}
// do something

2. Strategy pattern

There is such a scenario where different logics are used according to different parameters. In fact, this scenario is very common.
The most general implementation:

if (strategy. equals("fast")) {
  // fast execution
} else if (strategy. equals("normal")) {
  // execute normally
} else if (strategy. equals("smooth")) {
  // smooth execution
} else if (strategy. equals("slow")) {
  // execute slowly
}

Looking at the above code, there are 4 strategies and two optimization schemes.

2.1 Polymorphism

interface Strategy {
  void run() throws Exception;
}

class FastStrategy implements Strategy {
    @Override
    void run() throws Exception {
        // fast execution logic
    }
}

class NormalStrategy implements Strategy {
    @Override
    void run() throws Exception {
        // normal execution logic
    }
}

class SmoothStrategy implements Strategy {
    @Override
    void run() throws Exception {
        // smooth execution logic
    }
}

class SlowStrategy implements Strategy {
    @Override
    void run() throws Exception {
        // execute logic slowly
    }
}

The specific policy object is stored in a Map, and the optimized implementation

Strategy strategy = map. get(param);
strategy. run();

The above optimization scheme has a disadvantage. In order to quickly obtain the corresponding policy implementation, a map object is needed to save the policy. When adding a new policy, it needs to be manually added to the map, which is easy to be ignored.

2.2 Enumeration

I found that many students don’t know that methods can be defined in enumerations. Here, an enumeration representing the state is defined, and a run method can be implemented.

public enum Status {
    NEW(0) {
      @Override
      void run() {
        //do something
      }
    },
    RUNNABLE(1) {
      @Override
       void run() {
         //do something
      }
    };

    public int statusCode;

    abstract void run();

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

Redefine policy enumeration

public enum Strategy {
    fast {
      @Override
      void run() {
        //do something
      }
    },
    NORMAL {
      @Override
       void run() {
         //do something
      }
    },

    SMOOTH {
      @Override
       void run() {
         //do something
      }
    },

    SLOW {
      @Override
       void run() {
         //do something
      }
    };
    abstract void run();
}

The code after enumeration optimization is as follows

Strategy strategy = Strategy. valueOf(param);
strategy. run();

3. Learn to use Optional

Optional is mainly used for non-empty judgment. Since it is a new feature of jdk8, it is not used very much, but it is really cool to use.

Before use:

if (user == null) {
    //do action 1
} else {
    //do action2
}

If the logged-in user is empty, execute action 1, otherwise execute action 2. After using Optional optimization, the non-null check is more elegant, and the if operation is indirectly reduced.

Optional<User> userOptional = Optional.ofNullable(user);
userOptional.map(action1).orElse(action2);

4. Array tricks

From Google’s explanation, this is a programming model called table-driven method. Its essence is to query information from the table instead of logical statements. For example, there is such a scenario where the number of days in the current month is obtained through the month. It is only used as a case demonstration, and the data is not rigorous.

General implementation:

int getDays(int month){
    if (month == 1) return 31;
    if (month == 2) return 29;
    if (month == 3) return 31;
    if (month == 4) return 30;
    if (month == 5) return 31;
    if (month == 6) return 30;
    if (month == 7) return 31;
    if (month == 8) return 31;
    if (month == 9) return 30;
    if (month == 10) return 31;
    if (month == 11) return 30;
    if (month == 12) return 31;
}

optimized code

int monthDays[12] = {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int getDays(int month){
    return monthDays[--month];
}

Finish

If else, as an indispensable conditional statement in every programming language, will be used extensively in programming. It is generally recommended not to nest more than three layers. If a piece of code has too many if else nests, the readability of the code will drop rapidly, and the difficulty of later maintenance will also be greatly increased.

Viewpoint 2 (IT technology control):

Don’t pay too much attention to the number of if/else layers, but pay attention to whether the interface semantics are clear enough; simply reducing the number of if/else layers, and then tearing out a bunch of do_logic1, do_logic2… such interfaces is not helpful.

The execution process of any interface can be expressed as: input + internal state -> output. We discuss the following situations:

The input, internal state, and output are all simple, but the intermediate logic is complex. For example, a well-optimized numerical calculation program may need to adopt different strategies in different value ranges according to the input, and there are many logics used to deal with boundary values that cause problems (such as dividing by 0). In this case, if A large number of /else is unavoidable. Splitting some internal methods according to the steps is helpful, but it cannot completely solve the problem. The best approach in this case is to write a detailed document, starting from the most primitive mathematical model, and then indicating what calculation strategy to adopt under what circumstances, how to derive the strategy, knowing the specific form used in the code, and then Add a comment to the entire method and attach the document address, and add a comment to each branch to indicate which formula corresponds to the document. In this case, although the method is complicated, the semantics are clear. If you do not modify the implementation, you can understand the semantics. If you want to modify the implementation, you need to refer to the formula in the reference document.

The input is too complex, for example, the input has a bunch of different parameters, or there are various strange flags, and each flag has a different effect. In this case, it is first necessary to improve the abstraction level of the interface: if the interface has multiple different functions, it needs to be split into different interfaces; if the interface enters different branches according to different parameters, these parameters and corresponding branches need to be packaged in the Adapter, using The parameter place is rewritten as an Adapter interface, and different implementations are entered according to the type of Adapter passed in; if there is a complex parameter conversion relationship inside the interface, it needs to be rewritten as a lookup table. The main problem in this case is the abstraction of the interface itself. After a clearer abstraction, the implementation naturally does not have so many if/else.

The output is too complicated, too many things are calculated in one process to save trouble, and a bunch of flags are added to control whether to calculate or not for performance. In this case, it is necessary to decisively split the method into multiple different methods, and each method only returns what it needs. What if there are shared internal results between different calculations? If the internal result calculation does not form a bottleneck, just extract the internal method and call it separately in different processes; if you want to avoid repeated calculations, you can add an additional cache object as a parameter, the cache content is opaque to the user, and the user only guarantees Just use the same cache object for the same input, save the intermediate results in the cache during the calculation, and check whether there are any obtained results before the next calculation, so as to avoid repeated calculations.

The internal state is too complex. First check that the state is set up properly, is there something that should be an input parameter that is put into the internal state (for example, used to implicitly pass parameters between two different method calls)? Secondly, which aspects are controlled by these states, can they be grouped and implemented in different StateManagers? Third, draw a state transition diagram, try to divide the internal state into single-layer branches, and then implement them into methods such as on_xxx_state, and then call them through a single-layer switch or lookup table.

In fact, what usually needs to be optimized is the abstraction of the overall interface, not the implementation of a single interface. The unclear implementation of a single interface is usually caused by the different structure of the interface implementation and requirements.

Follow the B station number: Xiaoyu, come quickly, + q fan group: 725022484 to receive 300G high-quality programming materials for free