你還在 if...else?代碼這樣寫纔好看!

前言

if...else 是全部高級編程語言都有的必備功能。但現實中的代碼每每存在着過多的 if...else。雖然 if...else 是必須的,但濫用 if...else 會對代碼的可讀性、可維護性形成很大傷害,進而危害到整個軟件系統。如今軟件開發領域出現了不少新技術、新概念,但 if...else 這種基本的程序形式並無發生太大變化。使用好 if...else 不只對於如今,並且對於未來,都是十分有意義的。今天咱們就來看看如何「幹掉」代碼中的 if...else,還代碼以清爽。html

問題一:if…else 過多

問題表現

if...else 過多的代碼能夠抽象爲下面這段代碼。其中只列出5個邏輯分支,但實際工做中,能見到一個方法包含10個、20個甚至更多的邏輯分支的狀況。另外,if...else 過多一般會伴隨着另兩個問題:邏輯表達式複雜和 if...else 嵌套過深。對於後兩個問題,本文將在下面兩節介紹。本節先來討論 if...else 過多的狀況。java

if (condition1) {

} else if (condition2) {

} else if (condition3) {

} else if (condition4) {

} else {

}

一般,if...else 過多的方法,一般可讀性和可擴展性都很差。從軟件設計角度講,代碼中存在過多的 if...else 每每意味着這段代碼違反了違反單一職責原則和開閉原則。由於在實際的項目中,需求每每是不斷變化的,新需求也層出不窮。因此,軟件系統的擴展性是很是重要的。而解決 if...else 過多問題的最大意義,每每就在於提升代碼的可擴展性。程序員

如何解決

接下來咱們來看如何解決 if...else 過多的問題。下面我列出了一些解決方法。面試

    1. 表驅動
    1. 職責鏈模式
    1. 註解驅動
    1. 事件驅動
    1. 有限狀態機
    1. Optional
    1. Assert
    1. 多態

方法一:表驅動

介紹

對於邏輯表達模式固定的 if...else 代碼,能夠經過某種映射關係,將邏輯表達式用表格的方式表示;再使用表格查找的方式,找到某個輸入所對應的處理函數,使用這個處理函數進行運算。spring

適用場景

邏輯表達模式固定的 if...elseapache

實現與示例

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,這裏不作講解。後端

表的映射關係,能夠採用集中的方式,也能夠採用分散的方式,即每一個處理類自行註冊。也能夠經過配置文件的方式表達。總之,形式有不少。設計模式

還有一些問題,其中的條件表達式並不像上例中的那樣簡單,但稍加變換,一樣能夠應用表驅動。下面借用《編程珠璣》中的一個稅金計算的例子:api

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。故業務系統如採用此模式需自行實現相關核心功能。主要會涉及反射、職責鏈等技術。具體的實現這裏就不作演示了。

方法四:事件驅動

介紹

經過關聯不一樣的事件類型和對應的處理機制,來實現複雜的邏輯,同時達到解耦的目的。

適用場景

從理論角度講,事件驅動能夠看作是表驅動的一種,但從實踐角度講,事件驅動和前面提到的表驅動有多處不一樣。具體來講:

  1. 表驅動一般是一對一的關係;事件驅動一般是一對多;
  2. 表驅動中,觸發和執行一般是強依賴;事件驅動中,觸發和執行是弱依賴

正是上述二者不一樣,致使了二者適用場景的不一樣。具體來講,事件驅動可用於如訂單支付完成觸發庫存、物流、積分等功能。

實現與示例

實現方式上,單機的實踐驅動可使用 Guava、Spring 等框架實現。分佈式的則通常經過各類消息隊列方式實現。可是由於這裏主要討論的是消除 if...else,因此主要是面向單機問題域。由於涉及具體技術,因此此模式代碼不作演示。

方法五:有限狀態機

介紹

有限狀態機一般被稱爲狀態機(無限狀態機這個概念能夠忽略)。先引用維基百科上的定義:

有限狀態機(英語:finite-state machine,縮寫:FSM),簡稱狀態機,是表示有限個狀態以及在這些狀態之間的轉> 移和動做等行爲的數學模型。

其實,狀態機也能夠看作是表驅動的一種,其實就是當前狀態和事件二者組合與處理函數的一種對應關係。固然,處理成功以後還會有一個狀態轉移處理。

適用場景

雖然如今互聯網後端服務都在強調無狀態,但這並不意味着不能使用狀態機這種設計。其實,在不少場景中,如協議棧、訂單處理等功能中,狀態機有這其自然的優點。由於這些場景中自然存在着狀態和狀態的流轉。

實現與示例

實現狀態機設計首先須要有相應的框架,這個框架須要實現至少一種狀態機定義功能,以及對於的調用路由功能。狀態機定義可使用 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 以後:

1 Optional<String> strOptional = Optional.of("Hello World!");
2 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 了。

使用場景

一般用於各類參數校驗

擴展: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 的個數,可是會提升代碼的可讀性:

  1. 抽取方法
  2. 衛語句

方法一:抽取方法

** **

介紹

抽取方法是代碼重構的一種手段。定義很容易理解,就是將一段代碼抽取出來,放入另外一個單獨定義的方法。借

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 一、二、三、4 分別爲 true、false,請你們排列組合一下下面表達式的結果。

1 if ((condition1 && condition2 ) || ((condition2 || condition3) && condition4)) {
2   
3 }

我想沒人願意幹上面的事情。關鍵是,這一大坨表達式的含義是什麼?關鍵便在於,當不知道表達式的含義時,沒人願意推斷它的結果。

因此,表達式複雜,並不必定是錯。可是表達式難以讓人理解就很差了。

如何解決

對於 if...else 表達式複雜的問題,主要用代碼重構中的抽取方法、移動方法等手段解決。由於這些方法在《代碼重構》一書中都有介紹,因此這裏再也不重複。

總結

本文一個介紹了10種(算上擴展有12種)用於消除、簡化 if...else 的方法。還有一些方法,如經過策略模式、狀態模式等手段消除 if...else 在《重構與模式》一書中也有介紹。

正如前言所說,if...else 是代碼中的重要組成部分,可是過分、沒必要要地使用 if...else,會對代碼的可讀性、可擴展性形成負面影響,進而影響到整個軟件系統。

「幹掉」if...else 的能力高低反映的是程序員對軟件重構、設計模式、面向對象設計、架構模式、數據結構等多方面技術的綜合運用能力,反映的是程序員的內功。要合理使用 if...else,不能沒有設計,也不能過分設計。這些對技術的綜合、合理地運用都須要程序員在工做中不斷的摸索總結。

做者:艾瑞克·邵 來源:www.cnblogs.com/eric-shao/p/10115577.html

歡迎關注公衆號 【碼農開花】一塊兒學習成長 我會一直分享Java乾貨,也會分享免費的學習資料課程和麪試寶典 回覆:【計算機】【設計模式】【002】有驚喜哦

相關文章
相關標籤/搜索