程式中減少使用if語句的方法集錦

孫薇發表於2016-07-06

大約十年前,我聽說了反if的活動,覺得這個概念非常荒謬。如果不用if語句,又怎麼能寫出有用的程式呢?這簡直太荒謬了。

但之後你會開始思考:是否還記得上週你拼命想讀懂的深度巢狀程式碼?糟透了對麼?要是有辦法能簡化它該多好。

反if活動的網站上沒給出多少實用性建議,因此在本文中,作者將會提供一系列模式,也許你會用得上。但首先我們來關注一下if語句到底造成了什麼問題。

if語句的問題

if語句的第一個問題在於,通常出現if語句的程式碼很容易越改越糟。我們試著寫個新的if語句:

public void theProblem(boolean someCondition) {
        // SharedState

        if(someCondition) {
            // CodeBlockA
        } else {
            // CodeBlockB
        }
}

這時候還不算太糟,但已經存在一些問題了。在閱讀這段程式碼時,我必須得去檢視對同一個SharedState來說,CodeBlockA和CodeBlockB有什麼改動。最開始這段程式碼很容易閱讀,但隨著CodeBlock越來越多,耦合越來越複雜之後,就會很難讀。

上面這種CodeBlock進一步巢狀if語句與本地return的濫用情況也很常見,很難搞懂業務邏輯是選擇了哪種路徑。

if語句的第二個問題在於:複製時會有問題,也就是說,if語句缺失domain的概念。很容易由於在不需要的情況下,由於將內容放在一起而增加耦合性,造成程式碼難讀難改。

而第三個問題在於:開發者必須在頭腦中模擬執行實現情況——你得讓自己變成一臺小型電腦,從而造成腦細胞浪費。開發者的精力應當用來思考如何解決問題,而不是浪費在如何將複雜的程式碼分支結構編織在一起之上。

雖然想要直截了當地寫出替代方案,但首先我得強調這句話:

凡事中庸而行,尤其是中庸本身

if語句通常會讓程式碼更加複雜,但這不代表我們要完全拋棄if語句。我曾經看到過一些非常糟糕的程式碼,只是為了消除所有的if語句而刻意避開if語句。我們想要繞開這個誤區,
下面我給出的每種模式,都會給出使用範圍。

單獨的if語句如果不復制到其他地方,也許是不錯的句子。在複製if語句時,我們會希望預知危險的第六感起效。

在程式碼庫之外,在與危險的外部世界交流時,我們會想要驗證incoming response,並根據其作出相應的修改。但在自己的程式碼庫中,由於有可靠的gatekeeper把關,我覺得這是個很好的機會,我們可以嘗試使用簡單、更為豐富與強大的替代方案來實現。

模式1:布林引數(Boolean Params)

背景: 有方法在修改行為時使用了boolean。

public void example() {
    FileUtils.createFile("name.txt", "file contents", false);
    FileUtils.createFile("name_temp.txt", "file contents", true);
}

public class FileUtils {
    public static void createFile(String name, String contents, boolean temporary) {
        if(temporary) {
            // save temp file
        } else {
            // save permanent file
        }
    }
}

問題: 在看到這段程式碼時,實際上你是將兩個方法捆綁到一起,布林引數的出現讓你有機會在程式碼中定義一個概念。

適用範圍: 通常看到這種情況,如果在編譯時我們可以算出程式碼要採用哪種路徑,就可以放心使用這種模式。

解決方案: 將這個方法拆分成兩個新的方法,然後if就不見了。

public void example() {
    FileUtils.createFile("name.txt", "file contents");
    FileUtils.createTemporaryFile("name_temp.txt", "file contents");
}

public class FileUtils {
    public static void createFile(String name, String contents) {
        // save permanent file
    }

    public static void createTemporaryFile(String name, String contents) {
        // save temp file
    }
}

模式2:使用多型(Polymorphism)

背景: 根據型別switch時。

public class Bird {

    private enum Species {
        EUROPEAN, AFRICAN, NORWEGIAN_BLUE;
    }

    private boolean isNailed;
    private Species type;

    public double getSpeed() {
        switch (type) {
            case EUROPEAN:
                return getBaseSpeed();
            case AFRICAN:
                return getBaseSpeed() - getLoadFactor();
            case NORWEGIAN_BLUE:
                return isNailed ? 0 : getBaseSpeed();
            default:
                return 0;
        }
    }

    private double getLoadFactor() {
        return 3;
    }

    private double getBaseSpeed() {
        return 10;
    }
}

問題: 在新增新的型別時,我們必須要記得更新switch語句,此外隨著不同bird的概念新增進來,bird類的凝聚力越來越糟。

適用範圍:根據型別做單次切換是可行的,如果switch太多,在新增新型別時如果忘記更新現有隱藏型別中的所有switch,就會導致bug出現,8thlight部落格關於這種情況有詳細描述。

解決方案: 使用多型,新增新型別時大家都不會忘記新增相關行為。
注意:上例為了簡潔只寫了一個方法,但在有多個switch時更有用。

public abstract class Bird {

    public abstract double getSpeed();

    protected double getLoadFactor() {
        return 3;
    }

    protected double getBaseSpeed() {
        return 10;
    }
}

public class EuropeanBird extends Bird {
    public double getSpeed() {
        return getBaseSpeed();
    }
}

public class AfricanBird extends Bird {
    public double getSpeed() {
        return getBaseSpeed() - getLoadFactor();
    }
}

public class NorwegianBird extends Bird {
    private boolean isNailed;

    public double getSpeed() {
        return isNailed ? 0 : getBaseSpeed();
    }
}

模式3:NullObject/Optional

背景: 當外部請求理解程式碼庫的主要用途時,回答“查一下null的情況”。

public void example() {
    sumOf(null);
}

private int sumOf(List numbers) {
    if(numbers == null) {
        return 0;
    }

    return numbers.stream().mapToInt(i -> i).sum();
}

模式4:將內聯語句(Inline statements)轉為表示式

背景: 在計算布林表示式時,包含if語句樹。

public boolean horrible(boolean foo, boolean bar, boolean baz) {
    if (foo) {
        if (bar) {
            return true;
        }
    }

    if (baz) {
        return true;
    } else {
        return false;
    }
}

問題: 這種程式碼會導致開發者必須用大腦來模擬計算機對方法的處理。

適用範圍:很少有不適用的情況,像這樣的程式碼可以合成一行,或者拆成不同的部分。

解決方案: 將if語句樹合成單個表示式。

public boolean horrible(boolean foo, boolean bar, boolean baz) {
    return foo && bar || baz;
}

模式5:給出應對策略

背景:在呼叫一些其他程式碼時,無法確保路徑是成功的。

public class Repository {
    public String getRecord(int id) {
        return null; // cannot find the record
    }
}

public class Finder {
    public String displayRecord(Repository repository) {
        String record = repository.getRecord(123);
        if(record == null) {
            return "Not found";
        } else {
            return record;
        }
    }
}

問題: 這類if語句增加了處理同一個物件或者資料結構的時間,其中包含隱藏耦合——null的情況。其它物件可能會返回其他代表沒有結果的Magic value。

適用範圍:最好將這類if語句放在一個地方,由於不會重複,我們就能將為空物件的magic value刪除。

解決方案:針對被呼叫程式碼,給出應對策略。Ruby的Hash#fetch就是很好的案例,Java也用到了類似的方法。這種模式也可以用在刪除例外情況時。

private class Repository {
    public String getRecord(int id, String defaultValue) {
        String result = Db.getRecord(id);

        if (result != null) {
            return result;
        }

        return defaultValue;
    }
}

public class Finder {
    public String displayRecord(Repository repository) {
        return repository.getRecord(123, "Not found");
    }
}

祝探索愉快

希望這些模式對你現在處理的問題有幫助。我在重構程式碼增進理解時,發現這些方法都很有用。要記得並非所有if語句都是魔鬼,不過現代程式語言還有很多功能值得我們探索並使用。

相關文章