大約十年前,我聽說了反if的活動,覺得這個概念非常荒謬。如果不用if語句,又怎麼能寫出有用的程式呢?這簡直太荒謬了。
但之後你會開始思考:是否還記得上週你拼命想讀懂的深度巢狀程式碼?糟透了對麼?要是有辦法能簡化它該多好。
反if活動的網站上沒給出多少實用性建議,因此在本文中,作者將會提供一系列模式,也許你會用得上。但首先我們來關注一下if語句到底造成了什麼問題。
if語句的問題
if語句的第一個問題在於,通常出現if語句的程式碼很容易越改越糟。我們試著寫個新的if語句:
1 2 3 4 5 6 7 8 9 |
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。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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就不見了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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時。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
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時更有用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
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的情況”。
1 2 3 4 5 6 7 8 9 10 11 |
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語句樹。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public boolean horrible(boolean foo, boolean bar, boolean baz) { if (foo) { if (bar) { return true; } } if (baz) { return true; } else { return false; } } |
問題: 這種程式碼會導致開發者必須用大腦來模擬計算機對方法的處理。
適用範圍:很少有不適用的情況,像這樣的程式碼可以合成一行,或者拆成不同的部分。
解決方案: 將if語句樹合成單個表示式。
1 2 3 |
public boolean horrible(boolean foo, boolean bar, boolean baz) { return foo && bar || baz; } |
模式5:給出應對策略
背景:在呼叫一些其他程式碼時,無法確保路徑是成功的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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也用到了類似的方法。這種模式也可以用在刪除例外情況時。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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語句都是魔鬼,不過現代程式語言還有很多功能值得我們探索並使用。