Tips for eliminating if... else

Posted by adrianTNT on Thu, 03 Mar 2022 23:51:17 +0100

1, Smelly and long if... else

No more nonsense. First look at the following code

publicinterface IPay {  
    void pay();  
}  

@Service
publicclass AliaPay implements IPay {  
     @Override
     public void pay() {  
        System.out.println("===Launch Alipay payment===");  
     }  
}  

@Service
publicclass WeixinPay implements IPay {  
     @Override
     public void pay() {  
         System.out.println("===Initiate wechat payment===");  
     }  
}  
  
@Service
publicclass JingDongPay implements IPay {  
     @Override
     public void pay() {  
        System.out.println("===Initiate JD payment===");  
     }  
}  

@Service
publicclass PayService {  
     @Autowired
     private AliaPay aliaPay;  
     @Autowired
     private WeixinPay weixinPay;  
     @Autowired
     private JingDongPay jingDongPay;  
    
   
     public void toPay(String code) {  
         if ("alia".equals(code)) {  
             aliaPay.pay();  
         } elseif ("weixin".equals(code)) {  
              weixinPay.pay();  
         } elseif ("jingdong".equals(code)) {  
              jingDongPay.pay();  
         } else {  
              System.out.println("Payment method not found");  
         }  
     }  
}

The toPay method of PayService class is mainly used to initiate payment. According to different code s, it is decided to call the pay method of different payment classes (such as aliaPay) for payment.
What's wrong with this code? Maybe that's what some people do.
Imagine that if there are more and more payment methods, such as Baidu payment, meituan payment, UnionPay payment, etc., you need to change the code of toPay method and add new else... If judgment. More judgment will lead to more and more logic?

Obviously, it violates the six principles of design mode: opening and closing principle and single responsibility principle.

Opening and closing principle: open to extension and close to modification. That is to say, when adding new functions, we should try to change the existing code as little as possible.

Principle of single responsibility: as the name suggests, the logic is required to be as single as possible, not too complex and easy to reuse.

What can be done to solve this problem?

2, El if's magic trick

1. Using annotations
The reason why code is used to judge which payment class to use in the code is because code and payment class do not have a binding relationship. If the binding relationship exists, you can not judge.
Let's define an annotation first.

@Retention(RetentionPolicy.RUNTIME)  
@Target(ElementType.TYPE)  
public@interface PayCode {  

     String value();    
     String name();  
}

Add this note to all payment classes

@PayCode(value = "alia", name = "Alipay payment")  
@Service
publicclass AliaPay implements IPay {  

     @Override
     public void pay() {  
         System.out.println("===Launch Alipay payment===");  
     }  
}  

 
@PayCode(value = "weixin", name = "Wechat payment")  
@Service
publicclass WeixinPay implements IPay {  
 
     @Override
     public void pay() {  
         System.out.println("===Initiate wechat payment===");  
     }  
} 

 
@PayCode(value = "jingdong", name = "Jingdong payment")  
@Service
publicclass JingDongPay implements IPay {  
 
     @Override
     public void pay() {  
        System.out.println("===Initiate JD payment===");  
     }  
}

Then add the most critical classes:

@Service
publicclass PayService2 implements ApplicationListener<ContextRefreshedEvent> {  
 
     privatestatic Map<String, IPay> payMap = null;  
     
     @Override
     public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {  
         ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();  
         Map<String, Object> beansWithAnnotation = applicationContext.getBeansWithAnnotation(PayCode.class);  
        
         if (beansWithAnnotation != null) {  
             payMap = new HashMap<>();  
             beansWithAnnotation.forEach((key, value) ->{  
                 String bizType = value.getClass().getAnnotation(PayCode.class).value();  
                 payMap.put(bizType, (IPay) value);  
             });  
         }  
     }  
    
     public void pay(String code) {  
        payMap.get(code).pay();  
     }  
}

The PayService2 class implements the ApplicationListener interface, so that you can get an instance of ApplicationContext in the onApplicationEvent method. We then get the class annotated with PayCode and put it into a map. The key in the map is the value defined in the PayCode annotation, which is consistent with the code parameter. Value is an instance of the payment class.
In this way, you can get the payment class instance directly through code every time without using if... else judgment. If you want to add a new payment method, just mark the PayCode annotation on the payment class and define a new code.
Note: Codes in this way can have no business meaning and can be pure numbers, only without repetition.

2. Dynamic splice name
This method is mainly aimed at scenarios where code has business meaning.

@Service
publicclass PayService3 implements ApplicationContextAware {   
     private ApplicationContext applicationContext;  
     privatestaticfinal String SUFFIX = "Pay";  

     @Override
     public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {  
        this.applicationContext = applicationContext;  
     }  

     public void toPay(String payCode) {  
         ((IPay) applicationContext.getBean(getBeanName(payCode))).pay();  
     }  

     public String getBeanName(String payCode) {  
         return payCode + SUFFIX;  
     }  
}

We can see that the name of payment bean is spliced by code and suffix, such as aliaPay, weixinPay and jingDongPay. This requires special attention when naming the payment class. The preceding paragraph should be consistent with the code. The instance of the called payment class is obtained directly from the ApplicationContext instance. By default, the bean is a single instance and placed in a map in memory, so there will be no performance problems.
In particular, this method implements the ApplicationContextAware interface, which is different from the ApplicationListener interface above. It is to tell you that there are more than one methods to obtain ApplicationContext instances.

3. Template method judgment
Of course, in addition to the two methods described above, the source code implementation of spring also tells us another idea to solve the if... else problem.
Let's take a look at some of the source code of spring AOP and the wrap method of defaultadvisor adapter registry

public Advisor wrap(Object adviceObject) throws UnknownAdviceTypeException {  
     if (adviceObject instanceof Advisor) {  
        return (Advisor) adviceObject;  
     }  
     if (!(adviceObject instanceof Advice)) {  
        thrownew UnknownAdviceTypeException(adviceObject);  
     }  
     Advice advice = (Advice) adviceObject;  
     if (advice instanceof MethodInterceptor) {    
        returnnew DefaultPointcutAdvisor(advice);  
     }  
     for (AdvisorAdapter adapter : this.adapters) {  
         if (adapter.supportsAdvice(advice)) {  
             returnnew DefaultPointcutAdvisor(advice);  
         }  
     }  
     thrownew UnknownAdviceTypeException(advice);  
 }

Focus on the supportAdvice method. There are three classes that implement this method. Let's take a random class and have a look

class AfterReturningAdviceAdapter implements AdvisorAdapter, Serializable {  
 
     @Override
     public boolean supportsAdvice(Advice advice) {  
        return (advice instanceof AfterReturningAdvice);  
     }  
 
     @Override
     public MethodInterceptor getInterceptor(Advisor advisor) {  
        AfterReturningAdvice advice = (AfterReturningAdvice) advisor.getAdvice();  
        returnnew AfterReturningAdviceInterceptor(advice);  
     }   
}

The supportsadadvice method of this class is very simple. It just judges whether the type of advice is AfterReturningAdvice.
We should be inspired by what we see here.
In fact, we can do this by defining an interface or abstract class, in which there is a support method to judge whether the code passed by the parameter can be processed by ourselves. If it can be processed, go through the payment logic.

publicinterface IPay {  
     boolean support(String code);   
     void pay();  
}  

@Service
publicclass AliaPay implements IPay {   
     @Override
     public boolean support(String code) {  
        return"alia".equals(code);  
     }  
 
     @Override
     public void pay() {  
        System.out.println("===Launch Alipay payment===");  
     }  
}  
 
@Service
publicclass WeixinPay implements IPay {  
 
     @Override
     public boolean support(String code) {  
        return"weixin".equals(code);  
     }  
 
     @Override
     public void pay() {  
        System.out.println("===Initiate wechat payment===");  
     }  
}  

@Service
publicclass JingDongPay implements IPay {  
     @Override
     public boolean support(String code) {  
        return"jingdong".equals(code);  
     }  
 
     @Override
     public void pay() {  
        System.out.println("===Initiate JD payment===");  
     }  
}

Each payment class has a support method to judge whether the passed code is equal to its own definition.

@Service
publicclass PayService4 implements ApplicationContextAware, InitializingBean {  

     private ApplicationContext applicationContext;  
     private List<IPay> payList = null;  

     @Override
     public void afterPropertiesSet() throws Exception {  
         if (payList == null) {  
             payList = new ArrayList<>();  
             Map<String, IPay> beansOfType = applicationContext.getBeansOfType(IPay.class);  
 
             beansOfType.forEach((key, value) -> payList.add(value));  
         }  
     }  
 
     @Override
     public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {  
        this.applicationContext = applicationContext;  
     }  
 
     public void toPay(String code) {  
         for (IPay iPay : payList) {  
             if (iPay.support(code)) {  
                iPay.pay();  
             }  
         }  
     }  
}

In this code, first initialize the payment class instance that implements the IPay interface into a list set, and return to loop through the list set when calling the payment interface. If the code is the same as that defined by yourself, call the pay method of the current payment class instance.

4. Strategy + factory mode
This method is also used in scenarios where code has business implications.
The policy pattern defines a set of algorithms, encapsulates them one by one, and makes them interchangeable.
Factory mode is used to encapsulate and manage the creation of objects. It is a creation mode.

publicinterface IPay {
    void pay();
}

@Service
publicclass AliaPay implements IPay {

    @PostConstruct
    public void init() {
        PayStrategyFactory.register("aliaPay", this);
    }


    @Override
    public void pay() {
        System.out.println("===Launch Alipay payment===");
    }

}

@Service
publicclass WeixinPay implements IPay {

    @PostConstruct
    public void init() {
        PayStrategyFactory.register("weixinPay", this);
    }

    @Override
    public void pay() {
        System.out.println("===Initiate wechat payment===");
    }
}

@Service
publicclass JingDongPay implements IPay {

    @PostConstruct
    public void init() {
        PayStrategyFactory.register("jingDongPay", this);
    }

    @Override
    public void pay() {
        System.out.println("===Initiate JD payment===");
    }
}

publicclass PayStrategyFactory {

    privatestatic Map<String, IPay> PAY_REGISTERS = new HashMap<>();


    public static void register(String code, IPay iPay) {
        if (null != code && !"".equals(code)) {
            PAY_REGISTERS.put(code, iPay);
        }
    }


    public static IPay get(String code) {
        return PAY_REGISTERS.get(code);
    }
}

@Service
publicclass PayService3 {

    public void toPay(String code) {
        PayStrategyFactory.get(code).pay();
    }
}

The key of this code is that each subclass of PayHandler defines the next subclass of PayHandler to be executed, forming a chain call. This chain structure is assembled through PayHandlerChain.

6. Other ways to eliminate if... else
Of course, there are many scenarios in which if... else judgment is used in actual project development. The above is just a few of them. Here are other common scenarios.
1. Return different strings according to different numbers

public String getMessage(int code) {  
     if (code == 1) {  
        return"success";  
     } elseif (code == -1) {  
        return"fail";  
     } elseif (code == -2) {  
        return"Network Timeout ";  
     } elseif (code == -3) {  
        return"Parameter error";  
     }  
     thrownew RuntimeException("code error");  
}

In fact, this judgment is not necessary. It can be done with an enumeration

publicenum MessageEnum {  
     SUCCESS(1, "success"),  
     FAIL(-1, "fail"),  
     TIME_OUT(-2, "Network Timeout "),  
     PARAM_ERROR(-3, "Parameter error");  

     privateint code;  
     private String message;  

     MessageEnum(int code, String message) {  
         this.code = code;  
         this.message = message;  
     }  
   
     public int getCode() {  
        returnthis.code;  
     }  

     public String getMessage() {  
        returnthis.message;  
     }  
  
     public static MessageEnum getMessageEnum(int code) {  
        return Arrays.stream(MessageEnum.values()).filter(x -> x.code == code).findFirst().orElse(null);  
     }  
}

Adjust the calling method slightly

public String getMessage(int code) {  
     MessageEnum messageEnum = MessageEnum.getMessageEnum(code);  
     return messageEnum.getMessage();  
}

2. Judgment in the set

The getMessageEnum method in the above enumeration MessageEnum may be written like this if the java8 syntax is not used

public static MessageEnum getMessageEnum(int code) {  
     for (MessageEnum messageEnum : MessageEnum.values()) {  
         if (code == messageEnum.code) {  
            return messageEnum;  
         }  
     }  
     returnnull;  
}

For filtering data or finding methods in a collection, java8 has a simpler way to eliminate if... else judgment.

public static MessageEnum getMessageEnum(int code) {  
     return Arrays.stream(MessageEnum.values()).filter(x -> x.code == code).findFirst().orElse(null);  
}

3. Simple judgment
In fact, there is no need to write some simple if... else, which can be replaced by the ternary operator. For example:

public String getMessage2(int code) {  
     if(code == 1) {  
        return"success";  
     }  
     return"fail";  
}

Change to ternary operator:

public String getMessage2(int code) {  
    return code == 1 ? "success" : "fail";  
}

After modification, the code is more concise.

4. Judgment in spring

For the parameter exceptions, the sooner they are found, the better. Assert is provided in spring to help us detect whether the parameters are valid.

public void save(Integer code,String name) {  
     if(code == null) {
       throw Exception("code Cannot be empty");     
     } else {
         if(name == null) {
             throw Exception("name Cannot be empty");     
         } else {
             System.out.println("doSave");
         }
     }
 }

If there are many parameters, the if... else statement will be very long. At this time, if you use the Assert class to judge, the code will be much simplified:

public String save2(Integer code,String name) {      
     Assert.notNull(code,"code Cannot be empty"); 
     Assert.notNull(name,"name Cannot be empty"); 
     System.out.println("doSave");
 }

Of course, there are many other scenes that can be optimized if... else. I won't introduce them here one by one. Interested friends can leave me messages to discuss and study together.

Topics: Java Spring Spring Boot Design Pattern Back-end