策略/状态模式——如何拯救你的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();   
            }
        }
    }
    

全文完。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352

推荐阅读更多精彩内容