是否你有過這樣的體驗:天天寫着if (){...if(){..}}else{...}的代碼,天天腦海裏一直模擬的程序的運行場景。如果是那請看下面文章:↓
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就不見了。
模式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:將內聯語句(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;
}
模式4:給出應對策略 背景:在調用一些其他代碼時,無法確保路徑是成功的。
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");
}
}