策略/状态模式——如何拯救你的if-else代码

一、为什么会出现大量的if-else代码

当你使用if-else的时候,无非是以下的两种情况:

1.1 异常情况判断的需要

在很多情况下,我们在对一个对象进行操作之前,需要对其进行非空的判断,不然很容易出现NPE异常,代码示例如下:

Fruit fruit = new Fruit();
if(fruit != null){
    // 正常的处理逻辑
} else {
    // 异常的处理逻辑
}

1.2 不同处理逻辑的需要

还有一些情况,我们需要根据对象中某个属性的值不同,来决定走哪条具体的业务逻辑,代码示例如下:

Fruit fruit = new Fruit();
if(fruit == null){
    return;
}
if(fruit.getType == 1){
    // 处理逻辑1
}else if(fruit.getType == 2) {
    // 处理逻辑2
}else{
    // 处理逻辑3
}

大量的if-else代码容易让逻辑变得异常复杂,维护性和可靠性极差。

二、几种推荐的优化方法

幸运的是,前辈们已经总结出了很多有效改进if-else代码的方法,一起来看看哪一款最适合你。

2.1 合并条件表达式

如果你发现一段代码中,有多个不同的if-else里面处理的逻辑是相同的,那么就意味着这些分支其实是可以合并的,如下代码给出了示例:

if(age < 18){
    return false;
}
...
if(height < 165){
    return false;
}
...
if(weight > 100){
    return false;
}
...
return true;

在如上的代码示例中,我们看到前三个if里面的逻辑是相同的,那么我们就可以将这三个条件判断进行合并:

if(age < 18 || height < 165 || weight > 100){
    return false;
}
...
return true;

2.2 异常条件先退出

比如下面的代码示例,我们想要找同事讨论一个问题,那么需要首先判断是不是工作日,是的话还要判断他是不是请假了没来,如果来的话,还要判断他是不是正在忙,如果不忙的话,才能找他讨论。代码示例如下:

if(isWorkDay()){
    if(isAbsent()){
        return false;
    }else{
        if(isBusy()){
            return false;
        }else{
            return true;
        }
    }
}else{
    return false;
}

这样一段代码,是不是很难一下子弄清楚整个逻辑究竟是想干啥。现在,我们尝试把所有的异常条件都放在最前面,优先退出,然后优化后的结果如下:

if(!isWorkDay()){
    return false;
}
if(isAbsent()){
    return false;
}
if(isBusy()){
    return false;
}
return true;

这时候,我们发现,这段代码还可以使用合并条件表达式再次进行优化:

if(!isWorkDay() || isAbsent()() || isBusy()){
    return false;
}
return true;

如此,代码逻辑瞬间感觉清晰多了。

2.3 正常流程外移

如果主流程放在if-else里面,甚至放在好几层的if-else里面,估计也很难看清楚什么条件下才会执行它吧,我们先来看一个例子:

double cost = 0.0;
if(price > 100){
    if(weight > 50){
        cost = getAccount() - price * weight;
    }
}
return cost;

在这段代码中,计算cost的逻辑是主流程,我们应该尽量保持其在if代码块之外,优化后的结果如下:

if(price <= 100 || weight <= 50){
    return 0.0
}
return getAccount() - price * weight;

我们不但将主体代码外移了,还去除了临时变量,如此,就能很清楚地看到什么条件下返回什么结果。

2.4 逻辑封装成方法

当if-else中有大量的语句代码的时候,可以考虑将这些代码单独封装成一个方法,然后调用方法。

Fruit fruit = new Fruit();
double cost = 0.0;
if(fruit == null){
    return cost;
}
if(fruit.getType == 1){
    // 一大段代码1
}else if(fruit.getType == 2) {
    // 一大段代码2
}else{
    // 一大段代码3
}

优化后:

Fruit fruit = new Fruit();
double cost = 0.0;
if(fruit == null){
    return cost;
}
if(fruit.getType == 1){
    return getOneCost();
}else if(fruit.getType == 2) {
    return getTwoCost();
}else{
    return getThreeCost();
}
private double getOneCost(){
    // 一大段代码1
}
private double getTwoCost(){
    // 一大段代码2
}
private double getThreeCost(){
    // 一大段代码3
}

2.5 使用卫语句

卫语句是为了消除else,将所有的条件分支以平行的方式进行展示。

if(isWorkDay()){
    ...
}
if(isAbsent()){
    ...
}
if(isBusy()){
    ...
}
...

三、重构的建议

上述优化的建议都是比较简单的,且对代码的改动比较少。如果你期待更高层次,更加优雅的代码优化,就需要重构成如下两种设计模式的形式。

3.1 策略模式

一个典型的名单创建场景原代码逻辑如下:

if(customer == null) {
    return false;
}
if("TEL".equals(customer.getSource())){
    // 执行电话来源的名单创建过程
} else if("SMS".equals(customer.getSource())){
    // 执行短信来源的名单创建过程
} else if("WECHAT".equals(customer.getSource())){
    // 执行微信来源的名单创建过程
} else {
    // 执行默认来源的名单创建过程
}
return true;

我们可以看到,代码对不同的名单来源进行了不同的处理过程,但是随着名单来源的增加和每种名单来源处理逻辑的复杂化,代码的可读性和维护性就变得很差。

现在我们使用策略模式对其进行优化:

public abstract class CustomerCreateStrategy{
    public abstract void createCustomer();
    
    // 公共方法,不管那种来源的名单创建流程都需要调用
    public void recordCustomer(){
        // 执行记录名单创建的记录
    }
}
public class TelCustomerCreateStrategy extends CustomerCreateStrategy {
    @Override
    public void createCustomer(){
        // 执行电话来源的名单创建逻辑
    }
}

其它三种名单创建代码省略,都是大同小异的,然后还要创建一个环境类。

public class customerCreatorContext{
    // 需要保持对策略类的引用
    private CustomerCreateStrategy customerCreateStrategy;
    
    public customerCreatorContext(CustomerCreateStrategy customerCreateStrategy){
        this.customerCreateStrategy = customerCreateStrategy;
    }
    
    public void beginCreateCustomer(){
        this.customerCreateStrategy.createCustomer();
    }
}

最后,我们在使用的时候代码如下:

CustomerCreatorContext context = 
        new CustomerCreatorContext(new TelCustomerCreateStrategy());
context.beginCreateCustomer();

要点是,用户必须知道有哪些策略,而具体使用哪一种策略,是由用户自己决定的。

3.2 状态模式

提醒有不同的状态,在不同状态下允许做不同的操作,下面是原来的处理逻辑:

if(remind.getDays() < 1) {
    // 提醒生成时间小于1天的逻辑
} else if(remind.getDays() < 3) {
    // 提醒生成时间小于3天的逻辑
} else {
    // 提醒生成时间不小于3天的逻辑
}

这样的代码和上面策略模式演示的名单创建逻辑有同样的问题,下面我们使用状态模式来对其进行重构:

public abstract class RemindState{
    public abstract void notifyHandler();
    
    // 公共方法
    public void recordRemind(){
        // 执行记录逻辑
    }
}
public class OneDayRemindState extends RemindState {
    @Override
    public void notifyHandler(){
        // 一日内提醒通知处理人的逻辑
    }
}

其余的状态类省略,下面我们定义一个环境类。

public class RemindNotifier{
    // 需要保持对状态类的引用
    private RemindState remindState;
    
    public beginNotifyHandler(){
        // 环境类自己维护状态的变更
        Integer days = getPassedDays();
        if(days < 1){
           this.remindState = new OneDayRemindState();
        }else if(days < 3){
            this.remindState = new ThreeDayRemindState();
        }else{
            this.remindState = new ManyDayRemindState();
        }
        remindState.notifyHandler();
    }
}

最后,我们使用的时候是这样的:

// 每天通过定时任务启动如下逻辑
RemindNotifier rm = new RemindNotifier();
rm.beginNotifyHandler();

3.3 两种模式的区别

我们可以看到,策略模式和状态模式非常地相近,都是一个抽象类(接口)和多个具体类,再加上一个环境类。但是在使用过程中还是有以下区别:

  1. 策略模式需要使用者知道总共有哪些策略可以使用,状态模式则不需要;

  2. 策略模式需要使用者手动指定需要使用哪一种策略,状态模式则不需要;

  3. 策略模式适用于对象在整个生命周期内没有状态变化的情况,而状态模式则适用于对象多状态之间频繁变化的情况;

  4. 策略模式一旦选定策略开始执行,中间无法变更策略,而状态模式是可以的;

  5. 状态模式生命周期中变更状态可以由环境类决定,也可以由各个状态类之间相互改变,比如A状态在执行完操作后,自动将状态对象转换为下一个状态返回给环境类,环境类并不知晓状态发生变化,只是继续调用即可。

    public class OpenStatus implements Status{
        @Override
        public Status doSomeThing(){
            // 此处省略open状态的逻辑
            // 将状态翻转
            return new CloseStatus();
        }
    }
    
    public class CloseStatus implements Status{
        @Override
        public Status doSomeThing(){
            // 此处省略close状态的逻辑
            // 将状态翻转
            return new OpenStatus();
        }
    }
    
    public class SwitchContext{
        private Status status = new OpenStatus();
        public void start(){
            for(int i = 0; i < 10; i++){
                 status = status.doSomeThing();   
            }
        }
    }
    

全文完。

你可能感兴趣的:(策略/状态模式——如何拯救你的if-else代码)