點選上方“Java技術驛站”,選擇“置頂公眾號”。
有內涵、有價值的文章第一時間送達!
精品專欄
原文出自:https://www.jianshu.com/p/1db0bba283f0
前言
if…else 是所有高階程式語言都有的必備功能。但現實中的程式碼往往存在著過多的 if…else。雖然 if…else 是必須的,但濫用 if…else 會對程式碼的可讀性、可維護性造成很大傷害,進而危害到整個軟體系統。現在軟體開發領域出現了很多新技術、新概念,但 if…else 這種基本的程式形式並沒有發生太大變化。使用好 if…else 不僅對於現在,而且對於將來,都是十分有意義的。今天我們就來看看如何“幹掉”程式碼中的 if…else,還程式碼以清爽。
問題一:if…else 過多
問題表現
if…else 過多的程式碼可以抽象為下麵這段程式碼。其中只列出5個邏輯分支,但實際工作中,能見到一個方法包含10個、20個甚至更多的邏輯分支的情況。另外,if…else 過多通常會伴隨著另兩個問題:邏輯運算式複雜和 if…else 巢狀過深。對於後兩個問題,本文將在下麵兩節介紹。本節先來討論 if…else 過多的情況。
if (condition1) {
} else if (condition2) {
} else if (condition3) {
} else if (condition4) {
} else {
}
通常,if...else 過多的方法,通常可讀性和可擴充套件性都不好。從軟體設計角度講,程式碼中存在過多的 if...else 往往意味著這段程式碼違反了違反單一職責原則和開閉原則。因為在實際的專案中,需求往往是不斷變化的,新需求也層出不窮。所以,軟體系統的擴充套件性是非常重要的。而解決 if...else 過多問題的最大意義,往往就在於提高程式碼的可擴充套件性。
如何解決
接下來我們來看如何解決 if...else 過多的問題。下麵我列出了一些解決方法。
-
表驅動
-
職責鏈樣式
-
註解驅動
-
事件驅動
-
有限狀態機
-
Optional
-
Assert
-
多型
方法一:表驅動
介紹
對於邏輯表達樣式固定的 if...else 程式碼,可以透過某種對映關係,將邏輯運算式用表格的方式表示;再使用表格查詢的方式,找到某個輸入所對應的處理函式,使用這個處理函式進行運算。
適用場景
邏輯表達樣式固定的 if...else
實現與示例
if (param.equals(value1)) {
doAction1(someParams);
} else if (param.equals(value2)) {
doAction2(someParams);
} else if (param.equals(value3)) {
doAction3(someParams);
}
// ...
可重構為
Map, Function> action> actionMappings = new HashMap<>(); // 這裡泛型 ? 是為方便演示,實際可替換為你需要的型別
// When init
actionMappings.put(value1, (someParams) -> { doAction1(someParams)});
actionMappings.put(value2, (someParams) -> { doAction2(someParams)});
actionMappings.put(value3, (someParams) -> { doAction3(someParams)});
// 省略 null 判斷
actionMappings.get(param).apply(someParams);
上面的示例使用了 Java 8 的 Lambda 和 Functional Interface,這裡不做講解。
表的對映關係,可以採用集中的方式,也可以採用分散的方式,即每個處理類自行註冊。也可以透過配置檔案的方式表達。總之,形式有很多。
還有一些問題,其中的條件運算式並不像上例中的那樣簡單,但稍加變換,同樣可以應用表驅動。下麵借用《程式設計珠璣》中的一個稅金計算的例子:
if income <= 2200
tax = 0
else if income <= 2700
tax = 0.14 * (income - 2200)
else if income <= 3200
tax = 70 + 0.15 * (income - 2700)
else if income <= 3700
tax = 145 + 0.16 * (income - 3200)
......
else
tax = 53090 + 0.7 * (income - 102200)
對於上面的程式碼,其實只需將稅金的計算公式提取出來,將每一檔的標準提取到一個表格,在加上一個迴圈即可。具體重構之後的程式碼不給出,大家自己思考。
方法二:職責鏈樣式
介紹
當 if...else 中的條件運算式靈活多變,無法將條件中的資料抽象為表格並用統一的方式進行判斷時,這時應將對條件的判斷權交給每個功能元件。並用鏈的形式將這些元件串聯起來,形成完整的功能。
適用場景
條件運算式靈活多變,沒有統一的形式。
實現與示例
職責鏈的樣式在開源框架的 Filter、Interceptor 功能的實現中可以見到很多。下麵看一下通用的使用樣式:
重構前:
public void handle(request) {
if (handlerA.canHandle(request)) {
handlerA.handleRequest(request);
} else if (handlerB.canHandle(request)) {
handlerB.handleRequest(request);
} else if (handlerC.canHandle(request)) {
handlerC.handleRequest(request);
}
}
重構後:
public void handle(request) {
handlerA.handleRequest(request);
}
public abstract class Handler {
protected Handler next;
public abstract void handleRequest(Request request);
public void setNext(Handler next) { this.next = next; }
}
public class HandlerA extends Handler {
public void handleRequest(Request request) {
if (canHandle(request)) doHandle(request);
else if (next != null) next.handleRequest(request);
}
}
當然,示例中的重構前的程式碼為了表達清楚,做了一些類和方法的抽取重構。現實中,更多的是平鋪式的程式碼實現。
註:職責鏈的控制樣式
職責鏈樣式在具體實現過程中,會有一些不同的形式。從鏈的呼叫控制角度看,可分為外部控制和內部控制兩種。
外部控制不靈活,但是減少了實現難度。職責鏈上某一環上的具體實現不用考慮對下一環的呼叫,因為外部統一控制了。但是一般的外部控制也不能實現巢狀呼叫。如果有巢狀呼叫,並且希望由外部控制職責鏈的呼叫,實現起來會稍微複雜。具體可以參考 Spring Web Interceptor 機制的實現方法。
內部控制就比較靈活,可以由具體的實現來決定是否需要呼叫鏈上的下一環。但如果呼叫控制樣式是固定的,那這樣的實現對於使用者來說是不便的。
設計樣式在具體使用中會有很多變種,大家需要靈活掌握
方法三:註解驅動
介紹
透過 Java 註解(或其它語言的類似機制)定義執行某個方法的條件。在程式執行時,透過對比入參與註解中定義的條件是否匹配,再決定是否呼叫此方法。具體實現時,可以採用表驅動或職責鏈的方式實現。
適用場景
適合條件分支很多多,對程式擴充套件性和易用性均有較高要求的場景。通常是某個系統中經常遇到新需求的核心功能。
實現與示例
很多框架中都能看到這種樣式的使用,比如常見的 Spring MVC。因為這些框架很常用,demo 隨處可見,所以這裡不再上具體的演示程式碼了。
這個樣式的重點在於實現。現有的框架都是用於實現某一特定領域的功能,例如 MVC。故業務系統如採用此樣式需自行實現相關核心功能。主要會涉及反射、職責鏈等技術。具體的實現這裡就不做演示了。
方法四:事件驅動
介紹
透過關聯不同的事件型別和對應的處理機制,來實現複雜的邏輯,同時達到解耦的目的。
適用場景
從理論角度講,事件驅動可以看做是表驅動的一種,但從實踐角度講,事件驅動和前面提到的表驅動有多處不同。具體來說:
-
表驅動通常是一對一的關係;事件驅動通常是一對多;
-
表驅動中,觸發和執行通常是強依賴;事件驅動中,觸發和執行是弱依賴
正是上述兩者不同,導致了兩者適用場景的不同。具體來說,事件驅動可用於如訂單支付完成觸發庫存、物流、積分等功能。
實現與示例
實現方式上,單機的實踐驅動可以使用 Guava、Spring 等框架實現。分散式的則一般透過各種訊息佇列方式實現。但是因為這裡主要討論的是消除 if...else,所以主要是面向單機問題域。因為涉及具體技術,所以此樣式程式碼不做演示。
方法五:有限狀態機
介紹
有限狀態機通常被稱為狀態機(無限狀態機這個概念可以忽略)。先取用維基百科上的定義:
有限狀態機(英語:finite-state machine,縮寫:FSM),簡稱狀態機,是表示有限個狀態以及在這些狀態之間的轉移和動作等行為的數學模型。
其實,狀態機也可以看做是表驅動的一種,其實就是當前狀態和事件兩者組合與處理函式的一種對應關係。當然,處理成功之後還會有一個狀態轉移處理。
適用場景
雖然現在網際網路後端服務都在強調無狀態,但這並不意味著不能使用狀態機這種設計。其實,在很多場景中,如協議棧、訂單處理等功能中,狀態機有這其天然的優勢。因為這些場景中天然存在著狀態和狀態的流轉。
實現與示例
實現狀態機設計首先需要有相應的框架,這個框架需要實現至少一種狀態機定義功能,以及對於的呼叫路由功能。狀態機定義可以使用 DSL 或者註解的方式。原理不複雜,掌握了註解、反射等功能的同學應該可以很容易實現。
參考技術:
-
Apache Mina State Machine Apache Mina 框架,雖然在 IO 框架領域不及 Netty,但它卻提供了一個狀態機的功能。https://mina.apache.org/mina-project/userguide/ch14-state-machine/ch14-state-machine.html。有自己實現狀態機功能的同學可以參考其原始碼。
-
Spring State Machine Spring 子專案眾多,其中有個不顯山不露水的狀態機框架 —— Spring State Machine https://projects.spring.io/spring-statemachine/。可以透過 DSL 和註解兩種方式定義。
上述框架只是起到一個參考的作用,如果涉及到具體專案,需要根據業務特點自行實現狀態機的核心功能。
方法六:Optional
介紹
Java 程式碼中的一部分 if...else 是由非空檢查導致的。因此,降低這部分帶來的 if...else 也就能降低整體的 if...else 的個數。
Java 從 8 開始引入了 Optional 類,用於表示可能為空的物件。這個類提供了很多方法,用於相關的操作,可以用於消除 if...else。開源框架 Guava 和 Scala 語言也提供了類似的功能。
使用場景
有較多用於非空判斷的 if...else。
實現與示例
傳統寫法:
String str = "Hello World!";
if (str != null) {
System.out.println(str);
} else {
System.out.println("Null");
}
使用 Optional 之後:
Optional<String> strOptional = Optional.of("Hello World!");
strOptional.ifPresentOrElse(System.out::println, () -> System.out.println("Null"));
Optional 還有很多方法,這裡不一一介紹了。但請註意,不要使用 get()
和 isPresent()
方法,否則和傳統的 if...else 無異。
擴充套件:Kotlin Null Safety
Kotlin 帶有一個被稱為 Null Safety 的特性:
bob?.department?.head?.name
對於一個鏈式呼叫,在 Kotlin 語言中可以透過 ?.
避免空指標異常。如果某一環為 null,那整個鏈式運算式的值便為 null。
方法七:Assert 樣式
介紹
上一個方法適用於解決非空檢查場景所導致的 if...else,類似的場景還有各種引數驗證,比如還有字串不為空等等。很多框架類庫,例如 Spring、Apache Commons 都提供了工具裡,用於實現這種通用的功能。這樣大家就不必自行編寫 if...else 了。
-
Apache Commons Lang 中的 Validate 類:https://commons.apache.org/proper/commons-lang/javadocs/api-3.1/org/apache/commons/lang3/Validate.html
-
Spring 的 Assert 類:https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/util/Assert.html
使用場景
通常用於各種引數校驗
擴充套件:Bean Validation
類似上一個方法,介紹 Assert 樣式順便介紹一個有類似作用的技術 —— Bean Validation。Bean Validation 是 Java EE 規範中的一個。Bean Validation 透過在 Java Bean 上用註解的方式定義驗證標準,然後透過框架統一進行驗證。也可以起到了減少 if...else 的作用。
方法八:多型
介紹
使用面向物件的多型,也可以起到消除 if...else 的作用。在程式碼重構這本書中,對此也有介紹:https://refactoring.com/catalog/replaceConditionalWithPolymorphism.html
使用場景
連結中給出的示例比較簡單,無法體現適合使用多型消除 if...else 的具體場景。一般來說,當一個類中的多個方法都有類似於示例中的 if...else 判斷,且條件相同,那就可以考慮使用多型的方式消除 if...else。
同時,使用多型也不是徹底消除 if...else。而是將 if...else 合併轉移到了物件的建立階段。在建立階段的 if..,我們可以使用前面介紹的方法處理。
小結
上面這節介紹了 if...else 過多所帶來的問題,以及相應的解決方法。除了本節介紹的方法,還有一些其它的方法。比如,在《重構與樣式》一書中就介紹了“用 Strategy 替換條件邏輯”、“用 State 替換狀態改變條件陳述句”和“用 Command 替換條件排程程式”這三個方法。其中的“Command 樣式”,其思想同本文的“表驅動”方法大體一致。另兩種方法,因為在《重構與樣式》一書中已做詳細講解,這裡就不再重覆。
何時使用何種方法,取決於面對的問題的型別。上面介紹的一些適用場景,只是一些建議,更多的需要開發人員自己的思考。
問題二:if...else 巢狀過深
問題表現
if...else 多通常並不是最嚴重的的問題。有的程式碼 if...else 不僅個數多,而且 if...else 之間巢狀的很深,也很複雜,導致程式碼可讀性很差,自然也就難以維護。
if (condition1) {
action1();
if (condition2) {
action2();
if (condition3) {
action3();
if (condition4) {
action4();
}
}
}
}
if...else 巢狀過深會嚴重地影響程式碼的可讀性。當然,也會有上一節提到的兩個問題。
如何解決
上一節介紹的方法也可用用來解決本節的問題,所以對於上面的方法,此節不做重覆介紹。這一節重點一些方法,這些方法並不會降低 if...else 的個數,但是會提高程式碼的可讀性:
-
抽取方法
-
衛陳述句
方法一:抽取方法
介紹
抽取方法是程式碼重構的一種手段。定義很容易理解,就是將一段程式碼抽取出來,放入另一個單獨定義的方法。借用 https://refactoring.com/catalog/extractMethod.html 中的定義:
適用場景
if...else 巢狀嚴重的程式碼,通常可讀性很差。故在進行大型重構前,需先進行小幅調整,提高其程式碼可讀性。抽取方法便是最常用的一種調整手段。
實現與示例
重構前:
public void add(Object element) {
if (!readOnly) {
int newSize = size + 1;
if (newSize > elements.length) {
Object[] newElements = new Object[elements.length + 10];
for (int i = 0; i < size; i++) {
newElements[i] = elements[i];
}
elements = newElements
}
elements[size++] = element;
}
}
重構後:
public void add(Object element) {
if (readOnly) {
return;
}
if (overCapacity()) {
grow();
}
addElement(element);
}
方法二:衛陳述句
介紹
在程式碼重構中,有一個方法被稱為“使用衛陳述句替代巢狀條件陳述句”https://refactoring.com/catalog/replaceNestedConditionalWithGuardClauses.html。直接看程式碼:
double getPayAmount() {
double result;
if (_isDead) result = deadAmount();
else {
if (_isSeparated) result = separatedAmount();
else {
if (_isRetired) result = retiredAmount();
else result = normalPayAmount();
};
}
return result;
}
重構之後
double getPayAmount() {
if (_isDead) return deadAmount();
if (_isSeparated) return separatedAmount();
if (_isRetired) return retiredAmount();
return normalPayAmount();
}
使用場景
當看到一個方法中,某一層程式碼塊都被一個 if...else 完整控制時,通常可以採用衛陳述句。
問題三:if...else 運算式過於複雜
問題表現
if...else 所導致的第三個問題來自過於複雜的條件運算式。下麵給個簡單的例子,當 condition 1、2、3、4 分別為 true、false,請大家排列組合一下下麵運算式的結果。
if ((condition1 && condition2 ) || ((condition2 || condition3) && condition4)) {
}
我想沒人願意幹上面的事情。關鍵是,這一大坨運算式的含義是什麼?關鍵便在於,當不知道運算式的含義時,沒人願意推斷它的結果。
所以,運算式複雜,並不一定是錯。但是運算式難以讓人理解就不好了。
如何解決
對於 if...else 運算式複雜的問題,主要用程式碼重構中的抽取方法、移動方法等手段解決。因為這些方法在《程式碼重構》一書中都有介紹,所以這裡不再重覆。
總結
本文一個介紹了10種(算上擴充套件有12種)用於消除、簡化 if...else 的方法。還有一些方法,如透過策略樣式、狀態樣式等手段消除 if...else 在《重構與樣式》一書中也有介紹。
正如前言所說,if...else 是程式碼中的重要組成部分,但是過度、不必要地使用 if...else,會對程式碼的可讀性、可擴充套件性造成負面影響,進而影響到整個軟體系統。
“幹掉”if...else 的能力高低反映的是程式員對軟體重構、設計樣式、面向物件設計、架構樣式、資料結構等多方面技術的綜合運用能力,反映的是程式員的內功。要合理使用 if...else,不能沒有設計,也不能過度設計。這些對技術的綜合、合理地運用都需要程式員在工作中不斷的摸索總結。
【死磕 Spring】----- IOC 之 載入 Bean
【死磕 Spring】----- IOC 之 Spring 統一資源載入策略
【死磕 Spring】----- IOC 之深入理解 Spring IoC
如果你覺得文章不錯,歡迎點贊分享到朋友圈
▽
長按下方圖片識別二維碼
關註豬豬