策略/狀態(tài)模式——如何拯救你的if-else代碼

一、為什么會(huì)出現(xiàn)大量的if-else代碼

當(dāng)你使用if-else的時(shí)候,無(wú)非是以下的兩種情況:

1.1 異常情況判斷的需要

在很多情況下,我們?cè)趯?duì)一個(gè)對(duì)象進(jìn)行操作之前,需要對(duì)其進(jìn)行非空的判斷,不然很容易出現(xiàn)NPE異常,代碼示例如下:

Fruit fruit = new Fruit();
if(fruit != null){
    // 正常的處理邏輯
} else {
    // 異常的處理邏輯
}

1.2 不同處理邏輯的需要

還有一些情況,我們需要根據(jù)對(duì)象中某個(gè)屬性的值不同,來(lái)決定走哪條具體的業(yè)務(wù)邏輯,代碼示例如下:

Fruit fruit = new Fruit();
if(fruit == null){
    return;
}
if(fruit.getType == 1){
    // 處理邏輯1
}else if(fruit.getType == 2) {
    // 處理邏輯2
}else{
    // 處理邏輯3
}

大量的if-else代碼容易讓邏輯變得異常復(fù)雜,維護(hù)性和可靠性極差。

二、幾種推薦的優(yōu)化方法

幸運(yùn)的是,前輩們已經(jīng)總結(jié)出了很多有效改進(jìn)if-else代碼的方法,一起來(lái)看看哪一款最適合你。

2.1 合并條件表達(dá)式

如果你發(fā)現(xiàn)一段代碼中,有多個(gè)不同的if-else里面處理的邏輯是相同的,那么就意味著這些分支其實(shí)是可以合并的,如下代碼給出了示例:

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

在如上的代碼示例中,我們看到前三個(gè)if里面的邏輯是相同的,那么我們就可以將這三個(gè)條件判斷進(jìn)行合并:

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

2.2 異常條件先退出

比如下面的代碼示例,我們想要找同事討論一個(gè)問(wèn)題,那么需要首先判斷是不是工作日,是的話還要判斷他是不是請(qǐng)假了沒(méi)來(lái),如果來(lái)的話,還要判斷他是不是正在忙,如果不忙的話,才能找他討論。代碼示例如下:

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

這樣一段代碼,是不是很難一下子弄清楚整個(gè)邏輯究竟是想干啥。現(xiàn)在,我們嘗試把所有的異常條件都放在最前面,優(yōu)先退出,然后優(yōu)化后的結(jié)果如下:

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

這時(shí)候,我們發(fā)現(xiàn),這段代碼還可以使用合并條件表達(dá)式再次進(jìn)行優(yōu)化:

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

如此,代碼邏輯瞬間感覺(jué)清晰多了。

2.3 正常流程外移

如果主流程放在if-else里面,甚至放在好幾層的if-else里面,估計(jì)也很難看清楚什么條件下才會(huì)執(zhí)行它吧,我們先來(lái)看一個(gè)例子:

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

在這段代碼中,計(jì)算cost的邏輯是主流程,我們應(yīng)該盡量保持其在if代碼塊之外,優(yōu)化后的結(jié)果如下:

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

我們不但將主體代碼外移了,還去除了臨時(shí)變量,如此,就能很清楚地看到什么條件下返回什么結(jié)果。

2.4 邏輯封裝成方法

當(dāng)if-else中有大量的語(yǔ)句代碼的時(shí)候,可以考慮將這些代碼單獨(dú)封裝成一個(gè)方法,然后調(diào)用方法。

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
}

優(yōu)化后:

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 使用衛(wèi)語(yǔ)句

衛(wèi)語(yǔ)句是為了消除else,將所有的條件分支以平行的方式進(jìn)行展示。

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

三、重構(gòu)的建議

上述優(yōu)化的建議都是比較簡(jiǎn)單的,且對(duì)代碼的改動(dòng)比較少。如果你期待更高層次,更加優(yōu)雅的代碼優(yōu)化,就需要重構(gòu)成如下兩種設(shè)計(jì)模式的形式。

3.1 策略模式

一個(gè)典型的名單創(chuàng)建場(chǎng)景原代碼邏輯如下:

if(customer == null) {
    return false;
}
if("TEL".equals(customer.getSource())){
    // 執(zhí)行電話來(lái)源的名單創(chuàng)建過(guò)程
} else if("SMS".equals(customer.getSource())){
    // 執(zhí)行短信來(lái)源的名單創(chuàng)建過(guò)程
} else if("WECHAT".equals(customer.getSource())){
    // 執(zhí)行微信來(lái)源的名單創(chuàng)建過(guò)程
} else {
    // 執(zhí)行默認(rèn)來(lái)源的名單創(chuàng)建過(guò)程
}
return true;

我們可以看到,代碼對(duì)不同的名單來(lái)源進(jìn)行了不同的處理過(guò)程,但是隨著名單來(lái)源的增加和每種名單來(lái)源處理邏輯的復(fù)雜化,代碼的可讀性和維護(hù)性就變得很差。

現(xiàn)在我們使用策略模式對(duì)其進(jìn)行優(yōu)化:

public abstract class CustomerCreateStrategy{
    public abstract void createCustomer();
    
    // 公共方法,不管那種來(lái)源的名單創(chuàng)建流程都需要調(diào)用
    public void recordCustomer(){
        // 執(zhí)行記錄名單創(chuàng)建的記錄
    }
}
public class TelCustomerCreateStrategy extends CustomerCreateStrategy {
    @Override
    public void createCustomer(){
        // 執(zhí)行電話來(lái)源的名單創(chuàng)建邏輯
    }
}

其它三種名單創(chuàng)建代碼省略,都是大同小異的,然后還要?jiǎng)?chuàng)建一個(gè)環(huán)境類(lèi)。

public class customerCreatorContext{
    // 需要保持對(duì)策略類(lèi)的引用
    private CustomerCreateStrategy customerCreateStrategy;
    
    public customerCreatorContext(CustomerCreateStrategy customerCreateStrategy){
        this.customerCreateStrategy = customerCreateStrategy;
    }
    
    public void beginCreateCustomer(){
        this.customerCreateStrategy.createCustomer();
    }
}

最后,我們?cè)谑褂玫臅r(shí)候代碼如下:

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

要點(diǎn)是,用戶(hù)必須知道有哪些策略,而具體使用哪一種策略,是由用戶(hù)自己決定的。

3.2 狀態(tài)模式

提醒有不同的狀態(tài),在不同狀態(tài)下允許做不同的操作,下面是原來(lái)的處理邏輯:

if(remind.getDays() < 1) {
    // 提醒生成時(shí)間小于1天的邏輯
} else if(remind.getDays() < 3) {
    // 提醒生成時(shí)間小于3天的邏輯
} else {
    // 提醒生成時(shí)間不小于3天的邏輯
}

這樣的代碼和上面策略模式演示的名單創(chuàng)建邏輯有同樣的問(wèn)題,下面我們使用狀態(tài)模式來(lái)對(duì)其進(jìn)行重構(gòu):

public abstract class RemindState{
    public abstract void notifyHandler();
    
    // 公共方法
    public void recordRemind(){
        // 執(zhí)行記錄邏輯
    }
}
public class OneDayRemindState extends RemindState {
    @Override
    public void notifyHandler(){
        // 一日內(nèi)提醒通知處理人的邏輯
    }
}

其余的狀態(tài)類(lèi)省略,下面我們定義一個(gè)環(huán)境類(lèi)。

public class RemindNotifier{
    // 需要保持對(duì)狀態(tài)類(lèi)的引用
    private RemindState remindState;
    
    public beginNotifyHandler(){
        // 環(huán)境類(lèi)自己維護(hù)狀態(tài)的變更
        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();
    }
}

最后,我們使用的時(shí)候是這樣的:

// 每天通過(guò)定時(shí)任務(wù)啟動(dòng)如下邏輯
RemindNotifier rm = new RemindNotifier();
rm.beginNotifyHandler();

3.3 兩種模式的區(qū)別

我們可以看到,策略模式和狀態(tài)模式非常地相近,都是一個(gè)抽象類(lèi)(接口)和多個(gè)具體類(lèi),再加上一個(gè)環(huán)境類(lèi)。但是在使用過(guò)程中還是有以下區(qū)別:

  1. 策略模式需要使用者知道總共有哪些策略可以使用,狀態(tài)模式則不需要;

  2. 策略模式需要使用者手動(dòng)指定需要使用哪一種策略,狀態(tài)模式則不需要;

  3. 策略模式適用于對(duì)象在整個(gè)生命周期內(nèi)沒(méi)有狀態(tài)變化的情況,而狀態(tài)模式則適用于對(duì)象多狀態(tài)之間頻繁變化的情況;

  4. 策略模式一旦選定策略開(kāi)始執(zhí)行,中間無(wú)法變更策略,而狀態(tài)模式是可以的;

  5. 狀態(tài)模式生命周期中變更狀態(tài)可以由環(huán)境類(lèi)決定,也可以由各個(gè)狀態(tài)類(lèi)之間相互改變,比如A狀態(tài)在執(zhí)行完操作后,自動(dòng)將狀態(tài)對(duì)象轉(zhuǎn)換為下一個(gè)狀態(tài)返回給環(huán)境類(lèi),環(huán)境類(lèi)并不知曉狀態(tài)發(fā)生變化,只是繼續(xù)調(diào)用即可。

    public class OpenStatus implements Status{
        @Override
        public Status doSomeThing(){
            // 此處省略open狀態(tài)的邏輯
            // 將狀態(tài)翻轉(zhuǎn)
            return new CloseStatus();
        }
    }
    
    public class CloseStatus implements Status{
        @Override
        public Status doSomeThing(){
            // 此處省略close狀態(tài)的邏輯
            // 將狀態(tài)翻轉(zhuǎn)
            return new OpenStatus();
        }
    }
    
    public class SwitchContext{
        private Status status = new OpenStatus();
        public void start(){
            for(int i = 0; i < 10; i++){
                 status = status.doSomeThing();   
            }
        }
    }
    

全文完。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容