修煉碼德系列:簡化條件表達式

前言

與面向過程編程相比,面向對象編程的條件表達式相對來說已經比少了,因爲很多的條件行爲都可以被多態的機制處理掉;但是有時候我們還是會遇到一些小夥伴寫出來的條件表達式和麪向過程編程沒什麼差別,比如我遇到過這段代碼:

樣例

整段代碼有三層,每一層還有if-else,本身的這段代碼的邏輯就夠難以理解了,更加噁心的是這個方法的調用方以及調用的其他方法,同樣也是這樣的if-else嵌套幾層;
加之這段代碼還有一個很大的問題是傳入的參數對象,在內部以及調用的其他方法中被修改多次修改,這樣就更難懂了;靠普通人的單核CPU想看懂太難了,維護這段代碼我感覺身體被掏空

身體被掏空

有時候我們可能會遇到比較複雜的條件邏輯,需要我們想辦法把分成若干個小塊,讓分支邏輯和操作細節分離;看一個程序員的碼德如何,先看他的條件表達式是否夠簡潔易懂;今天我們來分享一下簡化條件表達式的常用方法,修煉自己的碼德;本文中大部分的例子來源於《重構改善既有代碼設計》


分解條件表達式

複雜的條件邏輯是最常導致複雜度上升的地方之一,另外如果分支內部邏輯也很多,最終我們會得到一個很大的函數,一個長的方法可讀性本身就會下降,所以我們需要把大的方法才分的多個的方法,爲每個方法取一個容易清楚表達實現內部邏輯的方法名,這樣可讀性就會上大大提高。

舉例:

if (date.before (SUMMER_START) || date.after(SUMMER_END)) {
    charge = quantity * _winterRate + _winterServiceCharge;
} else {
    charge = quantity * _summerRate
}

這種代碼很多人可能都覺得沒必要去提取方法,但是如果我們想要看懂這段代碼,還是必須的去想想才知道在做什麼;接下來我們修改一下

if (notSummer(date)) {
    charge = winterCharge(quantity);
} else {
    charge = summerCharge(quantity);
}

private boolean notSummer(Date date){
    date.before (SUMMER_START) || date.after(SUMMER_END)
}

private double summerCharge(int quantity) {
    return quantity * _summerRate;
}

private double winterCharge(int quantity) {
    return quantity * _winterRate + _winterServiceCharge;
}

這樣修改之後是不是很清楚,好的代碼本身不需要寫註釋(代碼具有自說明性),更不需要在方法內部寫任何註釋,有時候我們會看到有同學會在方法內部隔幾行就會寫一點註釋,這說明本身代碼的自說明性不夠好,可以通過剛纔這個例子的方式提高代碼的可讀性


合併條件表達式

當遇到一段代碼多個if條件判斷,但是條件內部的邏輯缺類似,我們可以把條件合併在一起,然後抽取方法。

舉例1:

double disabilityAmount () {
    if(_seniortiy <2 ) 
        return 0;
    if(_monthsDisabled > 12)
        return 0;
    if(_isPartTime)
        return 0;
    // 省略...
}

這裏的條件返回的結果都是一樣的,那麼我們先把條件合併起來

double disabilityAmount () {
    if(_seniortiy <2 || _monthsDisabled > 12 || _isPartTime) {
        return 0;
    }
    // 省略...
}

接下來我們再來把判斷條件判斷條件抽取成方法提高可讀性

double disabilityAmount () {
    if(isNotEligibleForDisableility()) {
        return 0;
    }
    // 省略...
}

boolean isNotEligibleForDisableility() {
    return _seniortiy <2 || _monthsDisabled > 12 || _isPartTime;
}

舉例2:

if(onVacation()) {
    if(lengthOfService() > 10) {
        return 2;
    }
}
return 1;

合併之後的代碼

if(onVacation() && lengthOfService() > 10){
    return 2
}
return 1;

接着我們可以使用三元操作符更加簡化,修改後的代碼:

return onVacation() && lengthOfService() > 10 ? 2 : 1;

通過這兩個例子我們可以看出,先把條件邏輯與分支邏輯抽離成不同的方法分離開,然後我們會發現提高代碼的可讀性是如此的簡單,得心應手;所以抽離好的方法是關鍵;我覺得此處應該有掌聲

我膨脹了


合併重複的條件片段

我們先來看一個例子,10歲以下的小朋友票價打5折

if(ageLt10(age)) {
    price = price * 0.5;
    doSomething();
} else {
    price = price * 1;
    doSomething();
}

我們發現不同的分支都執行了相同的末段代碼邏輯,這時候我們可以把這段代碼提取到條件判斷之外,這裏舉得例子較爲簡單,通常工作中遇到的可能不是這樣一個簡單的方法,而是很多行復雜的邏輯條件,我們可以先把這個代碼提取成一個方法,然後把這個方法的調用放在條件判斷之前或之後

修改之後的代碼

if(ageLt10(age)) {
    price = price * 0.5;
} else {
    price = price * 1;
}
doSomething();

當我們遇到try-catch中有相同的邏輯代碼,我們也可以使用這種方式處理


衛語句取代嵌套條件表達式

方法中一旦出現很深的嵌套邏輯讓人很難看懂執行的主線。當使用了if-else表示兩個分支都是同等的重要,都是主線流程;向下圖表達的一樣,
if-else

但是大多數時候我們會遇到只有一條主線流程,其他的都是個別的異常情況,在這種情況下使用if-else就不太合適,應該用衛語句取代嵌套表達式。
if-reture

舉例1:

在薪酬系統中,以特殊的規則處理死亡員工,駐外員工,退休員工的薪資,這些情況都是很少出現,不屬於正常邏輯;

double getPayAmount() {
    double result;
    if(isDead){
        result = deadAmount();
    } else {
        if(isSeparated) {
            result = separatedAmount();
        } else {
            if(isRetired) {
                result = retiredAmount();
            } else {
                result = normalPayAmount();
            }
        }
    }
    return result;
}

在這段代碼中,我們完全看不出正常流程是什麼,這些偶爾發生的情況把正常流程給掩蓋了,一旦發生了偶然情況,就應該直接返回,引導代碼的維護者去看一個沒用意義的else只會妨礙理解;讓我們用return來改造一下

double getPayAmount() {
    if(isDead){
        return deadAmount();
    }
    if(isSeparated) {
        return separatedAmount():
    }
    if(isRetired) {
        return retiredAmount();
    }
    return normalPayAmount();
}

多態取代條件表達式

有時候我們會遇到if-else-if或者switch-case這種結構,這樣的代碼不僅不夠整潔,遇到複雜邏輯也同樣難以理解。這種情況我們可以利用面向對象的多態來改造。

舉例:
假如你正在開發一款遊戲,需要寫一個獲取箭塔(Bartizan)、弓箭手(Archer)、坦克(Tank)***力的方法;經過兩個小時的努力終於完成了這個功能;開發完成後的代碼如下:

int attackPower(Attacker attacker) {
    switch (attacker.getType()) {
        case "Bartizan":
            return 100;
        case "Archer":
            return 50;
        case "Tank":
            return 800;
    }
    throw new RuntimeException("can not support the method");
}

經過自測後沒有任何問題,此時你的心情很爽

心情很爽

當你提交代碼交由領導review的時候,領導(心裏想着這點東西搞兩個小時,上班摸魚太明顯了吧)直接回復代碼實現不夠優雅,重寫

1. 枚舉多態

你看到這個回覆雖然心裏很不爽,但是你也沒辦法,畢竟還是要在這裏混飯喫的;嘴上還是的回答OK

你思考了一會想到了使用枚舉的多態來實現不就行了,說幹就幹,於是你寫了下一個版本

int attackPower(Attacker attacker) {
   return AttackerType.valueOf(attacker.getType()).getAttackPower();
}

enum AttackerType {
    Bartizan("箭塔") {
        @Override
        public int getAttackPower() {
            return 100;
        }
    },
    Archer("弓箭手") {
        @Override
        public int getAttackPower() {
            return 50;
        }
    },
    Tank("坦克") {
        @Override
        public int getAttackPower() {
            return 800;
        }
    };

    private String label;

    Attacker(String label) {
        this.label = label;
    }

    public String getLabel() {
        return label;
    }

    public int getAttackPower() {
        throw new RuntimeException("Can not support the method");
    }
}

這次再提交領導review,順利通過了,你心想總於get到了領導的點了

2. 類多態

沒想到你沒高興幾天,又接到個新的需求,這個獲取力的方法需要修改,根據者的等級不同而力也不同;你考慮到上次的版本力是固定的值,使用枚舉還比較合適,而這次的修改要根據者的本身等級來計算了,如果再使用枚舉估計是不合適的;同時你也想着上次簡單實現被領導懟了,這次如果還是在上次的枚舉版本上來實現,估計也不會有好結果;最後你決定使用類的多態來完成

int attackPower(Attacker attacker) {
    return attacker.getAttackPower();
}

interface Attacker {
    default int getAttackPower() {
        throw new RuntimeException("Can not support the method");
    }
}

class Bartizan implements Attacker {
    public int getAttackPower() {
        return 100 * getLevel();
    }
}

class Archer implements Attacker {
    public int getAttackPower() {
        return 50 * getLevel();
    }
}

class Tank implements Attacker {
    public int getAttackPower() {
        return 800 * getLevel();
    }
}

完成之後提交給領導review,領導笑了笑通過了代碼評審;

3. 策略模式

你本以爲這樣就結束了,結果計劃趕不上變化,遊戲上線後,效果不是太好,你又接到了一個需求變更,力的計算不能這麼粗暴,我們需要後臺配置規則,讓部分參加活動玩家的力根據規則提升。

你很生氣,心裏想着:沒聽說過殺死程序員不需要用槍嗎,改三次需求就可以了,MD這是想我死嗎。

改需求

生氣歸生氣,但是不敢表露出來,誰讓你是領導呢,那就開搞吧

考慮到本次的邏輯加入了規則,規則本身可以設計的簡單,也可以設計的很複雜,如果後期規則變得更加複雜,那麼整個***對象類中會顯得特別的臃腫,擴展性也不好,所以你這次不再使用類的多態來實現,考慮使用策略模式,完成後代碼如下:

//定義計算類的接口
interface AttackPowerCalculator {
    boolean support(Attacker attacker);

    int calculate(Attacker attacker);
}

//箭塔***力計算類
class BartizanAttackPowerCalculator implements AttackPowerCalculator {

    @Override
    public boolean support(Attacker attacker) {
        return "Bartizan".equals(attacker.getType());
    }

    @Override
    public int calculate(Attacker attacker) {
        //根據規則計算***力
        return doCalculate(getRule());
    }
}

//弓箭手***力計算類
class ArcherAttackPowerCalculator implements AttackPowerCalculator {

    @Override
    public boolean support(Attacker attacker) {
        return "Archer".equals(attacker.getType());
    }

    @Override
    public int calculate(Attacker attacker) {
        //根據規則計算***力
        return doCalculate(getRule());
    }
}

//坦克***力計算類
class TankAttackPowerCalculator implements AttackPowerCalculator {

    @Override
    public boolean support(Attacker attacker) {
        return "Tank".equals(attacker.getType());
    }

    @Override
    public int calculate(Attacker attacker) {
        //根據規則計算***力
        return doCalculate(getRule());
    }
}

//聚合所有計算類
class AttackPowerCalculatorComposite implements AttackPowerCalculator {
    List<AttackPowerCalculator> calculators = new ArrayList<>();

    public AttackPowerCalculatorComposite() {
        this.calculators.add(new TankAttackPowerCalculator());
        this.calculators.add(new ArcherAttackPowerCalculator());
        this.calculators.add(new BartizanAttackPowerCalculator());
    }

    @Override
    public boolean support(Attacker attacker) {
        return true;
    }

    @Override
    public int calculate(Attacker attacker) {
        for (AttackPowerCalculator calculator : calculators) {
            if (calculator.support(attacker)) {
                calculator.calculate(attacker);
            }
        }
        throw new RuntimeException("Can not support the method"); 
    }
}

//入口處通過調用聚合類來完成計算
int attackPower(Attacker attacker) {
    AttackPowerCalculator calculator = new AttackPowerCalculatorComposite();
    return calculator.calculate(attacker);
}

你再次提交代碼給領導review,領導看了很滿意,表揚你說:小夥子,不錯,進步很快嘛,給你點贊哦;你回答:感謝領導認可(心裏想那是當然,畢竟我已經摸清了你的點在哪裏了)

覺得本次你的這個功能完成的還比較滿意的,請點贊關注評論走起哦

驕傲

引入斷言

最後一個簡化條件表達式的操作是引入斷言,這部分比較簡單,並且Spring框架本身也提供了斷言的工具類,比如下面這段代碼:

public void getProjectLimit(String project){
    if(project == null){
        throw new RuntimeException("project can not null");
    }
    doSomething();
}

加入Spring的斷言後的代碼

public void getProjectLimit(String project){
    Assert.notNull(project,"project can not null");
    doSomething();
}

寫在最後
感謝大家可以耐心地讀到這裏。
當然,文中或許會存在或多或少的不足、錯誤之處,有建議或者意見也非常歡迎大家在評論交流。
最後,希望朋友們可以點贊評論關注三連,因爲這些就是我分享的全部動力來源


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章