1. 如何解決代碼中if…else 過多的問題

        共 1701字,需瀏覽 4分鐘

         ·

        2020-04-07 23:29



        作者:艾瑞克·邵?鏈接:https://www.cnblogs.com/eric-shao/p/10115577.html


        前言

        if...else 是所有高級編程語言都有的必備功能。但現(xiàn)實中的代碼往往存在著過多的 if...else。雖然 if...else 是必須的,但濫用 if...else 會對代碼的可讀性、可維護性造成很大傷害,進而危害到整個軟件系統(tǒng)。
        現(xiàn)在軟件開發(fā)領(lǐng)域出現(xiàn)了很多新技術(shù)、新概念,但 if...else 這種基本的程序形式并沒有發(fā)生太大變化。使用好 if...else 不僅對于現(xiàn)在,而且對于將來,都是十分有意義的。
        今天我們就來看看如何“干掉”代碼中的 if...else,還代碼以清爽。


        問題一:if…else 過多


        問題表現(xiàn)

        if...else 過多的代碼可以抽象為下面這段代碼。其中只列出5個邏輯分支,但實際工作中,能見到一個方法包含10個、20個甚至更多的邏輯分支的情況。
        另外,if...else 過多通常會伴隨著另兩個問題:邏輯表達式復(fù)雜和 if...else 嵌套過深。對于后兩個問題,本文將在下面兩節(jié)介紹。本節(jié)先來討論 if...else 過多的情況。?
        if?(condition1)?{

        }?else?if?(condition2)?{

        }?else?if?(condition3)?{

        }?else?if?(condition4)?{

        }?else?{

        }

        通常,if...else 過多的方法,通??勺x性和可擴展性都不好。從軟件設(shè)計角度講,代碼中存在過多的 if...else 往往意味著這段代碼違反了違反單一職責(zé)原則和開閉原則。
        因為在實際的項目中,需求往往是不斷變化的,新需求也層出不窮。所以,軟件系統(tǒng)的擴展性是非常重要的。而解決 if...else 過多問題的最大意義,往往就在于提高代碼的可擴展性。

        如何解決

        接下來我們來看如何解決 if...else 過多的問題。下面我列出了一些解決方法。
        1. 表驅(qū)動
        2. 職責(zé)鏈模式
        3. 注解驅(qū)動
        4. 事件驅(qū)動
        5. 有限狀態(tài)機
        6. Optional
        7. Assert
        8. 多態(tài)


        方法一:表驅(qū)動


        介紹


        對于邏輯表達模式固定的 if...else 代碼,可以通過某種映射關(guān)系,將邏輯表達式用表格的方式表示;再使用表格查找的方式,找到某個輸入所對應(yīng)的處理函數(shù),使用這個處理函數(shù)進行運算。

        適用場景

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

        實現(xiàn)與示例


        if?(param.equals(value1))?{
        ????doAction1(someParams);
        }?else?if?(param.equals(value2))?{
        ????doAction2(someParams);
        }?else?if?(param.equals(value3))?{
        ????doAction3(someParams);
        }
        //?...

        可重構(gòu)為
        Map?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,這里不做講解。表的映射關(guān)系,可以采用集中的方式,也可以采用分散的方式,即每個處理類自行注冊。也可以通過配置文件的方式表達??傊问接泻芏?。
        還有一些問題,其中的條件表達式并不像上例中的那樣簡單,但稍加變換,同樣可以應(yīng)用表驅(qū)動。下面借用《編程珠璣》中的一個稅金計算的例子:
        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)

        對于上面的代碼,其實只需將稅金的計算公式提取出來,將每一檔的標準提取到一個表格,在加上一個循環(huán)即可。具體重構(gòu)之后的代碼不給出,大家自己思考。


        方法二:職責(zé)鏈模式


        介紹


        當 if...else 中的條件表達式靈活多變,無法將條件中的數(shù)據(jù)抽象為表格并用統(tǒng)一的方式進行判斷時,這時應(yīng)將對條件的判斷權(quán)交給每個功能組件。并用鏈的形式將這些組件串聯(lián)起來,形成完整的功能。

        適用場景


        條件表達式靈活多變,沒有統(tǒng)一的形式。

        實現(xiàn)與示例


        職責(zé)鏈的模式在開源框架的 Filter、Interceptor 功能的實現(xiàn)中可以見到很多。下面看一下通用的使用模式:
        重構(gòu)前:
        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);
        ????}
        }

        重構(gòu)后:
        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);
        ??}
        }

        當然,示例中的重構(gòu)前的代碼為了表達清楚,做了一些類和方法的抽取重構(gòu)?,F(xiàn)實中,更多的是平鋪式的代碼實現(xiàn)。

        注:職責(zé)鏈的控制模式


        職責(zé)鏈模式在具體實現(xiàn)過程中,會有一些不同的形式。從鏈的調(diào)用控制角度看,可分為外部控制和內(nèi)部控制兩種。
        外部控制不靈活,但是減少了實現(xiàn)難度。職責(zé)鏈上某一環(huán)上的具體實現(xiàn)不用考慮對下一環(huán)的調(diào)用,因為外部統(tǒng)一控制了。但是一般的外部控制也不能實現(xiàn)嵌套調(diào)用。
        如果有嵌套調(diào)用,并且希望由外部控制職責(zé)鏈的調(diào)用,實現(xiàn)起來會稍微復(fù)雜。具體可以參考 Spring Web Interceptor 機制的實現(xiàn)方法。
        內(nèi)部控制就比較靈活,可以由具體的實現(xiàn)來決定是否需要調(diào)用鏈上的下一環(huán)。但如果調(diào)用控制模式是固定的,那這樣的實現(xiàn)對于使用者來說是不便的。
        設(shè)計模式在具體使用中會有很多變種,大家需要靈活掌握

        方法三:注解驅(qū)動


        介紹


        通過 Java 注解(或其它語言的類似機制)定義執(zhí)行某個方法的條件。在程序執(zhí)行時,通過對比入?yún)⑴c注解中定義的條件是否匹配,再決定是否調(diào)用此方法。具體實現(xiàn)時,可以采用表驅(qū)動或職責(zé)鏈的方式實現(xiàn)。

        適用場景


        適合條件分支很多多,對程序擴展性和易用性均有較高要求的場景。通常是某個系統(tǒng)中經(jīng)常遇到新需求的核心功能。

        實現(xiàn)與示例


        很多框架中都能看到這種模式的使用,比如常見的 Spring MVC。因為這些框架很常用,demo 隨處可見,所以這里不再上具體的演示代碼了。
        這個模式的重點在于實現(xiàn)。現(xiàn)有的框架都是用于實現(xiàn)某一特定領(lǐng)域的功能,例如 MVC。故業(yè)務(wù)系統(tǒng)如采用此模式需自行實現(xiàn)相關(guān)核心功能。主要會涉及反射、職責(zé)鏈等技術(shù)。具體的實現(xiàn)這里就不做演示了。

        方法四:事件驅(qū)動


        介紹


        通過關(guān)聯(lián)不同的事件類型和對應(yīng)的處理機制,來實現(xiàn)復(fù)雜的邏輯,同時達到解耦的目的。

        適用場景


        從理論角度講,事件驅(qū)動可以看做是表驅(qū)動的一種,但從實踐角度講,事件驅(qū)動和前面提到的表驅(qū)動有多處不同。具體來說:
        1. 表驅(qū)動通常是一對一的關(guān)系;事件驅(qū)動通常是一對多;
        2. 表驅(qū)動中,觸發(fā)和執(zhí)行通常是強依賴;事件驅(qū)動中,觸發(fā)和執(zhí)行是弱依賴

        正是上述兩者不同,導(dǎo)致了兩者適用場景的不同。具體來說,事件驅(qū)動可用于如訂單支付完成觸發(fā)庫存、物流、積分等功能。

        實現(xiàn)與示例


        實現(xiàn)方式上,單機的實踐驅(qū)動可以使用 Guava、Spring 等框架實現(xiàn)。分布式的則一般通過各種消息隊列方式實現(xiàn)。但是因為這里主要討論的是消除 if...else,所以主要是面向單機問題域。因為涉及具體技術(shù),所以此模式代碼不做演示。


        方法五:有限狀態(tài)機


        介紹


        有限狀態(tài)機通常被稱為狀態(tài)機(無限狀態(tài)機這個概念可以忽略)。先引用維基百科上的定義:
        有限狀態(tài)機(英語:finite-state machine,縮寫:FSM),簡稱狀態(tài)機,是表示有限個狀態(tài)以及在這些狀態(tài)之間的轉(zhuǎn)移和動作等行為的數(shù)學(xué)模型。
        其實,狀態(tài)機也可以看做是表驅(qū)動的一種,其實就是當前狀態(tài)和事件兩者組合與處理函數(shù)的一種對應(yīng)關(guān)系。當然,處理成功之后還會有一個狀態(tài)轉(zhuǎn)移處理。

        適用場景


        雖然現(xiàn)在互聯(lián)網(wǎng)后端服務(wù)都在強調(diào)無狀態(tài),但這并不意味著不能使用狀態(tài)機這種設(shè)計。其實,在很多場景中,如協(xié)議棧、訂單處理等功能中,狀態(tài)機有這其天然的優(yōu)勢。因為這些場景中天然存在著狀態(tài)和狀態(tài)的流轉(zhuǎn)。

        實現(xiàn)與示例


        實現(xiàn)狀態(tài)機設(shè)計首先需要有相應(yīng)的框架,這個框架需要實現(xiàn)至少一種狀態(tài)機定義功能,以及對于的調(diào)用路由功能。狀態(tài)機定義可以使用 DSL 或者注解的方式。原理不復(fù)雜,掌握了注解、反射等功能的同學(xué)應(yīng)該可以很容易實現(xiàn)。
        參考技術(shù):
        • Apache Mina State Machine
          Apache Mina 框架,雖然在 IO 框架領(lǐng)域不及 Netty,但它卻提供了一個狀態(tài)機的功能。https://mina.apache.org/mina-project/userguide/ch14-state-machine/ch14-state-machine.html。有自己實現(xiàn)狀態(tài)機功能的同學(xué)可以參考其源碼。
        • Spring State Machine
          Spring 子項目眾多,其中有個不顯山不露水的狀態(tài)機框架 —— Spring State Machine?https://projects.spring.io/spring-statemachine/??梢酝ㄟ^ DSL 和注解兩種方式定義。

        上述框架只是起到一個參考的作用,如果涉及到具體項目,需要根據(jù)業(yè)務(wù)特點自行實現(xiàn)狀態(tài)機的核心功能。

        方法六:Optional

        介紹


        Java 代碼中的一部分 if...else 是由非空檢查導(dǎo)致的。因此,降低這部分帶來的 if...else 也就能降低整體的 if...else 的個數(shù)。
        Java 從 8 開始引入了 Optional 類,用于表示可能為空的對象。這個類提供了很多方法,用于相關(guān)的操作,可以用于消除 if...else。開源框架 Guava 和 Scala 語言也提供了類似的功能。

        使用場景


        有較多用于非空判斷的 if...else。

        實現(xiàn)與示例


        傳統(tǒng)寫法:
        String?str?=?"Hello?World!";
        if?(str?!=?null)?{
        ????System.out.println(str);
        }?else?{
        ????System.out.println("Null");
        }

        使用 Optional 之后:
        1?Optional?strOptional?=?Optional.of("Hello?World!");
        2?strOptional.ifPresentOrElse(System.out::println,?()?->?System.out.println("Null"));
        ?Optional 還有很多方法,這里不一一介紹了。但請注意,不要使用 get()?和 isPresent()?方法,否則和傳統(tǒng)的 if...else 無異。

        擴展:Kotlin Null Safety


        Kotlin 帶有一個被稱為 Null Safety 的特性:bob?.department?.head?.name
        對于一個鏈式調(diào)用,在 Kotlin 語言中可以通過 ?. 避免空指針異常。如果某一環(huán)為 null,那整個鏈式表達式的值便為 null。

        方法七:Assert 模式


        介紹


        上一個方法適用于解決非空檢查場景所導(dǎo)致的 if...else,類似的場景還有各種參數(shù)驗證,比如還有字符串不為空等等。很多框架類庫,例如 Spring、Apache Commons 都提供了工具里,用于實現(xiàn)這種通用的功能。這樣大家就不必自行編寫 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


        使用場景


        通常用于各種參數(shù)校驗

        擴展:Bean Validation


        類似上一個方法,介紹 Assert 模式順便介紹一個有類似作用的技術(shù) —— Bean Validation。Bean Validation 是 Java EE 規(guī)范中的一個。Bean Validation 通過在 Java Bean 上用注解的方式定義驗證標準,然后通過框架統(tǒng)一進行驗證。也可以起到了減少 if...else 的作用。

        方法八:多態(tài)

        介紹


        使用面向?qū)ο蟮亩鄳B(tài),也可以起到消除 if...else 的作用。在代碼重構(gòu)這本書中,對此也有介紹:https://refactoring.com/catalog/replaceConditionalWithPolymorphism.html


        使用場景


        鏈接中給出的示例比較簡單,無法體現(xiàn)適合使用多態(tài)消除 if...else 的具體場景。一般來說,當一個類中的多個方法都有類似于示例中的 if...else 判斷,且條件相同,那就可以考慮使用多態(tài)的方式消除 if...else。
        同時,使用多態(tài)也不是徹底消除 if...else。而是將 if...else 合并轉(zhuǎn)移到了對象的創(chuàng)建階段。在創(chuàng)建階段的 if..,我們可以使用前面介紹的方法處理。

        小結(jié)


        上面這節(jié)介紹了 if...else 過多所帶來的問題,以及相應(yīng)的解決方法。除了本節(jié)介紹的方法,還有一些其它的方法。
        比如,在《重構(gòu)與模式》一書中就介紹了“用 Strategy 替換條件邏輯”、“用 State 替換狀態(tài)改變條件語句”和“用 Command 替換條件調(diào)度程序”這三個方法。其中的“Command 模式”,其思想同本文的“表驅(qū)動”方法大體一致。
        另兩種方法,因為在《重構(gòu)與模式》一書中已做詳細講解,這里就不再重復(fù)。
        何時使用何種方法,取決于面對的問題的類型。上面介紹的一些適用場景,只是一些建議,更多的需要開發(fā)人員自己的思考。

        ??

        問題二:if…else 嵌套過深


        問題表現(xiàn)

        if...else 多通常并不是最嚴重的的問題。有的代碼 if...else 不僅個數(shù)多,而且 if...else 之間嵌套的很深,也很復(fù)雜,導(dǎo)致代碼可讀性很差,自然也就難以維護。
        if?(condition1)?{
        ????action1();
        ????if?(condition2)?{
        ????????action2();
        ????????if?(condition3)?{
        ????????????action3();
        ????????????if?(condition4)?{
        ????????????????action4();
        ????????????}
        ????????}
        ????}
        }

        if...else 嵌套過深會嚴重地影響代碼的可讀性。當然,也會有上一節(jié)提到的兩個問題。


        如何解決


        上一節(jié)介紹的方法也可用用來解決本節(jié)的問題,所以對于上面的方法,此節(jié)不做重復(fù)介紹。這一節(jié)重點一些方法,這些方法并不會降低 if...else 的個數(shù),但是會提高代碼的可讀性:
        1. 抽取方法
        2. 衛(wèi)語句


        方法一:抽取方法


        介紹


        抽取方法是代碼重構(gòu)的一種手段。定義很容易理解,就是將一段代碼抽取出來,放入另一個單獨定義的方法。借用?https://refactoring.com/catalog/extractMethod.html?中的定義:


        適用場景

        if...else 嵌套嚴重的代碼,通常可讀性很差。故在進行大型重構(gòu)前,需先進行小幅調(diào)整,提高其代碼可讀性。抽取方法便是最常用的一種調(diào)整手段。


        實現(xiàn)與示例


        重構(gòu)前:
        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?????????newElements[i]?=?elements[i];
        ??????}

        ??????elements?=?newElements
        ????}
        ????elements[size++]?=?element;
        ??}
        }

        重構(gòu)后:
        public?void?add(Object?element)?{
        ??if?(readOnly)?{
        ????return;
        ??}

        ??if?(overCapacity())?{
        ????grow();
        ??}

        ??addElement(element);
        }

        方法二:衛(wèi)語句


        介紹


        在代碼重構(gòu)中,有一個方法被稱為“使用衛(wèi)語句替代嵌套條件語句”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;
        }

        重構(gòu)之后
        double?getPayAmount()?{
        ????if?(_isDead)?return?deadAmount();
        ????if?(_isSeparated)?return?separatedAmount();
        ????if?(_isRetired)?return?retiredAmount();
        ????return?normalPayAmount();
        }

        使用場景


        當看到一個方法中,某一層代碼塊都被一個 if...else 完整控制時,通??梢圆捎眯l(wèi)語句。

        ?

        問題三:if…else 表達式過于復(fù)雜


        問題表現(xiàn)

        if...else 所導(dǎo)致的第三個問題來自過于復(fù)雜的條件表達式。下面給個簡單的例子,當 condition 1、2、3、4 分別為 true、false,請大家排列組合一下下面表達式的結(jié)果。
        1?if?((condition1?&&?condition2?)?||?((condition2?||?condition3)?&&?condition4))?{
        2???
        3?}

        我想沒人愿意干上面的事情。關(guān)鍵是,這一大坨表達式的含義是什么?關(guān)鍵便在于,當不知道表達式的含義時,沒人愿意推斷它的結(jié)果。
        所以,表達式復(fù)雜,并不一定是錯。但是表達式難以讓人理解就不好了。


        如何解決


        對于 if...else 表達式復(fù)雜的問題,主要用代碼重構(gòu)中的抽取方法、移動方法等手段解決。因為這些方法在《代碼重構(gòu)》一書中都有介紹,所以這里不再重復(fù)。

        ?

        總結(jié)

        本文一個介紹了10種(算上擴展有12種)用于消除、簡化 if...else 的方法。還有一些方法,如通過策略模式、狀態(tài)模式等手段消除 if...else 在《重構(gòu)與模式》一書中也有介紹。
        正如前言所說,if...else 是代碼中的重要組成部分,但是過度、不必要地使用 if...else,會對代碼的可讀性、可擴展性造成負面影響,進而影響到整個軟件系統(tǒng)。
        “干掉”if...else 的能力高低反映的是程序員對軟件重構(gòu)、設(shè)計模式、面向?qū)ο笤O(shè)計、架構(gòu)模式、數(shù)據(jù)結(jié)構(gòu)等多方面技術(shù)的綜合運用能力,反映的是程序員的內(nèi)功。
        要合理使用 if...else,不能沒有設(shè)計,也不能過度設(shè)計。這些對技術(shù)的綜合、合理地運用都需要程序員在工作中不斷的摸索總結(jié)。

        有道無術(shù),術(shù)可成;有術(shù)無道,止于術(shù)

        歡迎大家關(guān)注Java之道公眾號


        好文章,我在看??

        瀏覽 35
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
          
          

            1. 亚洲丝袜足交 | 魅影直播游客免登录 | 骚逼影视 | 有坂深雪av一区福利中出 | 成人无码影片在线观看 |