首頁>技術>

對於業務開發來說,業務邏輯的複雜是必然的。隨著業務發展,需求只會越來越複雜,為了考慮到各種各樣的情況,程式碼中不可避免的會出現很多 if-else。

圖片來自 Pexels

一旦程式碼中 if-else 過多,就會大大的影響其可讀性和可維護性。

首先可讀性,不言而喻,過多的 if-else 程式碼和巢狀,會使閱讀程式碼的人很難理解到底是什麼意思。尤其是那些沒有註釋的程式碼。

其次是可維護性,因為 if-else 特別多,想要新加一個分支的時候,就會很難新增,極其容易影響到其他的分支。

筆者曾經看到過一個支付的核心應用,這個應用支援了很多業務的線上支付功能,但是每個業務都有很多定製的需求,所以很多核心的程式碼中都有一大坨 if-else。

每個新業務需要定製的時候,都把自己的 if 放到整個方法的最前面,以保證自己的邏輯可以正常執行。這種做法,後果可想而知。

其實,if-else 是有辦法可以消除掉的,其中比較典型的並且使用廣泛的就是藉助策略模式和工廠模式,準確的說是利用這兩個設計模式的思想,徹底消滅程式碼中的 if-else。

本文就結合這兩種設計模式,介紹如何消除 if-else,並且,還會介紹如何和 Spring 框架結合,這樣讀者看完本文之後就可以立即應用到自己的專案中。

本文涉及到一些程式碼,但是作者儘量用通俗的例子和虛擬碼等形式使內容不那麼枯燥。

噁心的 if-else

假設我們要做一個外賣平臺,有這樣的需求:

外賣平臺上的某家店鋪為了促銷,設定了多種會員優惠,其中包含超級會員折扣 8 折、普通會員折扣 9 折和普通使用者沒有折扣三種。希望使用者在付款的時候,根據使用者的會員等級,就可以知道使用者符合哪種折扣策略,進而進行打折,計算出應付金額。隨著業務發展,新的需求要求專屬會員要在店鋪下單金額大於 30 元的時候才可以享受優惠。接著,又有一個變態的需求,如果使用者的超級會員已經到期了,並且到期時間在一週內,那麼就對使用者的單筆訂單按照超級會員進行折扣,並在收銀臺進行強提醒,引導使用者再次開通會員,而且折扣只進行一次。

那麼,我們可以看到以下虛擬碼:

public BigDecimal calPrice(BigDecimal orderPrice, String buyerType) { if (使用者是專屬會員) { if (訂單金額大於30元) { returen 7折價格; } } if (使用者是超級會員) { return 8折價格; } if (使用者是普通會員) { if(該使用者超級會員剛過期並且尚未使用過臨時折扣){ 臨時折扣使用次數更新(); returen 8折價格; } return 9折價格; } return 原價; } 

以上,就是對於這個需求的一段價格計算邏輯,使用虛擬碼都這麼複雜,如果是真的寫程式碼,那複雜度可想而知。

這樣的程式碼中,有很多 if-else,並且還有很多的 if-else 的巢狀,無論是可讀性還是可維護性都非常低。那麼,如何改善呢?

策略模式

接下來,我們嘗試引入策略模式來提升程式碼的可維護性和可讀性。

首先,定義一個介面:

/** * @author mhcoding */ public interface UserPayService { /** * 計算應付價格 */ public BigDecimal quote(BigDecimal orderPrice); } 

接著定義幾個策略類:

/** * @author mhcoding */ public class ParticularlyVipPayService implements UserPayService { @Override public BigDecimal quote(BigDecimal orderPrice) { if (消費金額大於30元) { return 7折價格; } } } public class SuperVipPayService implements UserPayService { @Override public BigDecimal quote(BigDecimal orderPrice) { return 8折價格; } } public class VipPayService implements UserPayService { @Override public BigDecimal quote(BigDecimal orderPrice) { if(該使用者超級會員剛過期並且尚未使用過臨時折扣){ 臨時折扣使用次數更新(); returen 8折價格; } return 9折價格; } } 

引入了策略之後,我們可以按照如下方式進行價格計算:

/** * @author mhcoding */ public class Test { public static void main(String[] args) { UserPayService strategy = new VipPayService(); BigDecimal quote = strategy.quote(300); System.out.println("普通會員商品的最終價格為:" + quote.doubleValue()); strategy = new SuperVipPayService(); quote = strategy.quote(300); System.out.println("超級會員商品的最終價格為:" + quote.doubleValue()); } } 

以上,就是一個例子,可以在程式碼中 New 出不同的會員的策略類,然後執行對應的計算價格的方法。

但是,真正在程式碼中使用,比如在一個 Web 專案中使用,上面這個 Demo 根本沒辦法直接用。

首先,在 Web 專案中,上面我們創建出來的這些策略類都是被 Spring 託管的,我們不會自己去 New 一個例項出來。

其次,在 Web 專案中,如果真要計算價格,也是要事先知道使用者的會員等級,比如從資料庫中查出會員等級,然後根據等級獲取不同的策略類執行計算價格方法。

那麼,Web 專案中真正的計算價格的話,虛擬碼應該是這樣的:

/** * @author mhcoding */ public BigDecimal calPrice(BigDecimal orderPrice,User user) { String vipType = user.getVipType(); if (vipType == 專屬會員) { //虛擬碼:從Spring中獲取超級會員的策略物件 UserPayService strategy = Spring.getBean(ParticularlyVipPayService.class); return strategy.quote(orderPrice); } if (vipType == 超級會員) { UserPayService strategy = Spring.getBean(SuperVipPayService.class); return strategy.quote(orderPrice); } if (vipType == 普通會員) { UserPayService strategy = Spring.getBean(VipPayService.class); return strategy.quote(orderPrice); } return 原價; } 

通過以上程式碼,我們發現,程式碼可維護性和可讀性好像是好了一些,但是好像並沒有減少 if-else 啊。

但是,策略模式的使用上,還是有一個比較大的缺點的:客戶端必須知道所有的策略類,並自行決定使用哪一個策略類。這就意味著客戶端必須理解這些演算法的區別,以便適時選擇恰當的演算法類。

也就是說,雖然在計算價格的時候沒有 if-else 了,但是選擇具體的策略的時候還是不可避免的還是要有一些 if-else。

另外,上面的虛擬碼中,從 Spring 中獲取會員的策略物件我們是虛擬碼實現的,那麼程式碼到底該如何獲取對應的 Bean 呢?

接下來我們看如何藉助 Spring 和工廠模式,解決上面這些問題。

工廠模式

為了方便我們從 Spring 中獲取 UserPayService 的各個策略類,我們建立一個工廠類:

/** * @author mhcoding */ public class UserPayServiceStrategyFactory { private static Map<String,UserPayService> services = new ConcurrentHashMap<String,UserPayService>(); public static UserPayService getByUserType(String type){ return services.get(type); } public static void register(String userType,UserPayService userPayService){ Assert.notNull(userType,"userType can't be null"); services.put(userType,userPayService); } } 

這個 UserPayServiceStrategyFactory 中定義了一個 Map,用來儲存所有的策略類的例項,並提供一個 getByUserType 方法,可以根據型別直接獲取對應的類的例項。還有一個 Register 方法,這個後面再講。

有了這個工廠類之後,計算價格的程式碼即可得到大大的優化:

/** * @author mhcoding */ public BigDecimal calPrice(BigDecimal orderPrice,User user) { String vipType = user.getVipType(); UserPayService strategy = UserPayServiceStrategyFactory.getByUserType(vipType); return strategy.quote(orderPrice); } 

以上程式碼中,不再需要 if-else 了,拿到使用者的 vip 型別之後,直接通過工廠的 getByUserType 方法直接呼叫就可以了。

通過策略+工廠,我們的程式碼很大程度的優化了,大大提升了可讀性和可維護性。

但是,上面還遺留了一個問題,那就是 UserPayServiceStrategyFactory 中用來儲存所有的策略類的例項的 Map 是如何被初始化的?各個策略的例項物件如何塞進去的呢?

Spring Bean 的註冊

還記得我們前面定義的 UserPayServiceStrategyFactory 中提供了的 Register 方法嗎?他就是用來註冊策略服務的。

接下來,我們就想辦法呼叫 Register 方法,把 Spring 通過 IOC 創建出來的 Bean 註冊進去就行了。

這種需求,可以借用 Spring 中提供的 InitializingBean 介面,這個介面為 Bean 提供了屬性初始化後的處理方法。

它只包括 afterPropertiesSet 方法,凡是繼承該介面的類,在 Bean 的屬性初始化後都會執行該方法。

那麼,我們將前面的各個策略類稍作改造即可:

/** * @author mhcoding */ @Service public class ParticularlyVipPayService implements UserPayService,InitializingBean { @Override public BigDecimal quote(BigDecimal orderPrice) { if (消費金額大於30元) { return 7折價格; } } @Override public void afterPropertiesSet() throws Exception { UserPayServiceStrategyFactory.register("ParticularlyVip",this); } } @Service public class SuperVipPayService implements UserPayService ,InitializingBean{ @Override public BigDecimal quote(BigDecimal orderPrice) { return 8折價格; } @Override public void afterPropertiesSet() throws Exception { UserPayServiceStrategyFactory.register("SuperVip",this); } } @Service public class VipPayService implements UserPayService,InitializingBean { @Override public BigDecimal quote(BigDecimal orderPrice) { if(該使用者超級會員剛過期並且尚未使用過臨時折扣){ 臨時折扣使用次數更新(); returen 8折價格; } return 9折價格; } @Override public void afterPropertiesSet() throws Exception { UserPayServiceStrategyFactory.register("Vip",this); } } 

只需要每一個策略服務的實現類都實現 InitializingBean 介面,並實現其 afterPropertiesSet 方法,在這個方法中呼叫 UserPayServiceStrategyFactory.register 即可。

這樣,在 Spring 初始化的時候,當建立 VipPayService、SuperVipPayService 和 ParticularlyVipPayService 的時候,會在 Bean 的屬性初始化之後,把這個 Bean 註冊到 UserPayServiceStrategyFactory 中。

以上程式碼,其實還是有一些重複程式碼的,這裡面還可以引入模板方法模式進一步精簡,這裡就不展開了。

還有就是,UserPayServiceStrategyFactory.register 呼叫的時候,第一個引數需要傳一個字串,這裡的話其實也可以優化掉。

比如使用列舉,或者在每個策略類中自定義一個 getUserType 方法,各自實現即可。

總結

本文,我們通過策略模式、工廠模式以及 Spring 的 InitializingBean,提升了程式碼的可讀性以及可維護性,徹底消滅了一坨 if-else。

文中的這種做法,大家可以立刻嘗試起來,這種實踐,是我們日常開發中經常用到的,而且還有很多衍生的用法,也都非常好用。有機會後面再介紹。

其實,如果讀者們對策略模式和工廠模式了解的話,文中使用的並不是嚴格意義上面的策略模式和工廠模式。

首先,策略模式中重要的 Context 角色在這裡面是沒有的,沒有 Context,也就沒有用到組合的方式,而是使用工廠代替了。

另外,這裡面的 UserPayServiceStrategyFactory 其實只是維護了一個 Map,並提供了 Register 和 Get 方法而已,而工廠模式其實是幫忙建立物件的,這裡並沒有用到。

所以,讀者不必糾結於到底是不是真的用了策略模式和工廠模式。而且,這裡面也再擴充套件一句,所謂的 GOF 23 種設計模式,無論從哪本書或者哪個部落格看,都是簡單的程式碼示例,但是我們日常開發很多都是基於 Spring 等框架的,根本沒辦法直接用的。

所以,對於設計模式的學習,重要的是學習其思想,而不是程式碼實現!!!希望通過這樣的文章,讀者可以真正的在程式碼中使用上設計模式。

28607
最新評論
  • 1 #

    看了這排版,我還是用if else吧

  • 2 #

    if else明顯可讀性和可維護性都比多型好啊。多型不是拿來幹這個的。

  • 3 #

    還用if else,不做巢狀就是了

  • 4 #

    其實早就能想到需求會各種變化,就直接用case

  • 5 #

    用數學模型解決才是王道

  • 6 #

    被外賣平臺的需求逗笑了

  • 7 #

    還不如截個圖,排版成這樣子怎麼看?

  • 8 #

    這麼長的文章,說得很有道理的樣子,我信你

  • 9 #

    敢把排版整理下麼?

  • 10 #

    這排版,我選擇放棄

  • 11 #

    我湊 你這叫我們怎麼看

  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 移動開發架構選型大PK