改寫 If-Else 讓 Code 更專業

原文:https://medium.com/swlh/5-ways-to-replace-if-else-statements-857c0ff19357

一開始就必須先說:if / else 通常極差的選項。

if / else 的寫法會導致複雜的設計,難以閱讀的 code,並有可能極難重構。

if / else 確實已經是慣用的邏輯分支的寫法。通常初學者一開始學 coding 就必然學到 if / else 的邏輯分支寫法。可惜的是大部分開發者極少從這樣初學者的寫法進階到更合適的分支策略。

 

一但擁有了 if / else 這把鐵鎚,眼中看到的任何事都成了釘子。

 

初學者與進階開發的一個區別在於能否判斷何時該使用更合適的方法。下面我們會列出一些技巧與 patterns 來改寫 if / else 的一些不好的寫法。

以下難度會逐漸遞增。

1 完全不需要的 else 區塊

這或許會是大部分初階開發者最感到罪惡的寫法。若你覺得 if / else 是個好寫法時,以下就是一個很好的說明範例。

public void performOperation(int input) {
    if (input > 5) {
        // do something
    } else {
        // do something else
    }
}

這很明顯的可以直接移除 else 區塊來簡化。

public void performOperation(int input) {
    if (input > 5) {
        // do something
        return;
    } 

    // do something else
}

是不是看起來變專業了?

2 對變數賦值

當需要根據外部輸入條件決定變數值時,停止使用無意義的 if / else。改成具有更高可讀性的方法。

public static String determineGender(int input) {
    String gender = "";
    
    if (input == 0) {
        gender = "male";
    } else if (input == 1) {
        gender = "woman"; 
    } else {
        gender = "unknown";
    }

    return gender;
}

即便這 code 看來簡單,但寫法就是可怕的。初步來說,if / else 應該被換成 switch / case。不過比 switch / case 更好的做法是完全移除 else if / 以及 else。

public static String determineGender(int input) {
    if (input == 0) return "male";
    if (input == 1) return "woman"; 

    return "unknown";
}

去除 else if 與 else 後,我們就得到了一個乾淨且具更高可讀性的 code。特別要注意這裡的修改同時也做了 early return 的修改。畢竟當任一條件滿足時後續的條件測試都已經不再具有意義。

3 前置條件檢查

很多時候我們會發現當輸入值是不預期值域範圍時,持續往下執行通常不是個有意義的事。

假設以下的 determineGender 預期的合理輸入值僅有 0 或 1。

// input must be 0 or 1
public String determineGender(int input) {
    // continue executing logic
}

以這例子來說,應該要對前置條件先做檢查來判斷是否持續往下執行。

套用防衛樣式 (guard clauses) 後,每個函式一開始都會先確認輸入條件滿足再往下。

public String determineGender(int input) {
    if (input < 0) throw new IllegalArgumentException();
    if (input > 1) throw new IllegalArgumentException(); 

    return input == 0 ? "male" : "woman";
}

經過前置條件檢查後,我們已經可以確保主邏輯僅會有我們預期的值。

主邏輯的 if 也改寫為 ternary,因為此時已經完全沒有返回 “Unknown” fallback 值的意義了。

4 轉換 if / else 為查表法 – 完全避開 if / else

假設現在我們要根據某些條件執行對應的操作。一般可能會是如下例的寫法。

public void performOp(String opName) {
    if (opName.equals("op1") {
        // something 
    } else if (opName.equals("op2") {
        // something else
    } else {
        // default path
    }
}

若要加入一個新的操作,在這樣的寫法下就是再塞一個 else if block。簡單,但從維護的角度來看卻不盡然是一個好設計。

若後續會再加入其他操作是已知條件下,我們可以將此類寫法重構為以字典來記錄操作碼對應的 operation。

public void performOp(String opName) {
    Map<String, Function<String, String>> ops = new HashMap<>(); 
    ops["op1"] = { /* op1 operation */ };
    ops["op2"] = { /* op2 operation */ };

    ops.get(opName).apply(opName);
}

以字典法改寫後,可讀性大幅提升,更容易理解此段的目的。(map 放在 method 內僅是為了範例,正常來說應該是 instance level 或是 class level 的 map。

5 擴充設計 – 完全避開 if / else

以下討論的是更為進階的範例。

也就是說這個方法比較算是『企業』型的解決方案,而不是一般的『就用這個來取代 if / else 吧』的場景。

作為一個初階開發者,當面對擴充需求時,很常會直覺地做出 if / else 的設計,並且在需要擴充時不斷地放入新的 else-if 區塊。

拿下面這個範例來看。這邊的目標是想要將一個訂單的 instance 表示成各種不同的數據表示方法。目前是兩種字串表示方式:一個是 JSON 而另一個是純文字。在目前在目前可見的 scope 來看,使用 if / else 並不是太大的問題。

public String printOrder(Order order, String formatType) {
    // Guard clauses left out for brevity 

    String result = ""; 
    
    if ("json".equals(formatType) { 
        result = order.toJsonString();
    } else of ("plainText".equals(formatType)) {
        result = String.format("Id: %s\nSum: %d", order.id, order.sum);
    } else {
        result = "unknown format";
    }

    return result; 
}

若是已知這個部分有可能需要進一步擴充,這個方法就一定不是這麼適用。

這個設計不僅違反了物件導向設計的開放封閉原則,也可能造成後續維護的隱憂。

比較合理的方法是一個符合物件導向設計的 SOLID 原則的設計:動態偵測型別的方法,也就是 strategy pattern。

重構此例為 Strategy Pattern 的步驟如下:
  1. 將每一個邏輯分支抽出為具有共通介面的 strategy 類別
  2. 動態找出所有實作這個共通介面的類別
  3. 根據輸入值決定使用哪個策略來執行

以此方式實作的 code 看起來會較 if / else 的方式更多或複雜。但在大型軟體設計中,以此方式設計時,後續擴充僅需專注於新 strategy 的開發,這個地方僅是一個基礎框架設計,不太會需要更動,符合封閉開放原則。(這裡比較偏向 design pattern 的範疇,就先不列出改過的 code sample)


以下非原文內容,為個人想法。

當時讀此篇文章時,看到最後一點以 strategy pattern 改寫時突然勾起十多年前初入職場時前期的幾個 task 之一就是以 strategy 改寫原本大量的邏輯判斷分支。的確是個進階的改法,但改動前要很確定自己知道自己在做什麼,尤其是真的有擴充需求時才需要改動,否則只會是為了 refactor 而引入了不必要風險的改動。



Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *