不吹牛,我有好幾種方法可以徹底幹掉討厭的if-else(附源碼)

 

我們在web開發中,經常使用數據庫表中的字段作爲“標記”來表示多個“狀態”,比如:

我們就以某寶的在線購物流程爲例進行分析。在訂單表中,使用zt字段來表示定單的狀態,常見的狀態就有:

 

當我們想按條件查詢各個類型的訂單的時候,只需要一個接口,在前端傳入相應的狀態碼就可以了。在dao層大概也就是通過如下的語句進行查詢:


 
select * from orders where zt = #{zt} 

如何纔能有很高的擴展性?

假設有這麼幾個“不成需求的需求”:

  1. 我想讓待收貨的訂單按照訂單發貨時間或者預計送達時間排序,其他的暫且按照訂單創建時間排序吧
  2. 想將“待收貨”的狀態區分開,分爲“用戶未收到貨”和“用戶收到貨但是未點擊確認收貨按紐”兩種狀態

常規方式如何解決?

  • ​ 需求一(不同的狀態處理方式不同):這個很容易的,在sevice層添加一個判斷就可以,其他的代碼不用改,代碼如下:

 
// 2 表示待收貨 if(zt == 2){ //按照需求,按照訂單發貨時間或者預計送達時間排序 }else{ //其他狀態的訂單,全部按照訂單創建時間排序 } 

​ 上邊這個代碼的修改量已經很小了,但是如果我要把和種不同的狀態訂單全部按照不同的排序方式排序呢?你可能會寫如下代碼


 
if(zt==0){ // 待付款的訂單處理代碼... }else if(zt==1){ }else if(zt==2){ }else if(zt==3){ }else if(zt==4){ } 

上邊代碼太low了,有些小夥伴可能會使用switch進行優化(這裏就不寫代碼了,因爲和上邊並沒有任何區別)。

  • 需求二(添加一個新的狀態表示):這也很easy啊,直接在上邊的if-else或者switch代碼中添加新的狀態判斷不就好了。

但是這樣會產生什麼樣的問題呢?

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

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

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

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

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

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

思考如何幹掉if-else?

上邊的方式可以完成我們的需求,但是有以下幾點不足:


 
1. 面對“各種各樣奇怪的需求”,我們要頻繁地修改上邊的代碼,時間久了,豈不成了渣渣。甚至我們自己都不願意再去看這些代碼了; 2. 如果新增加一個狀態表示,也就是給zt字段新的狀態含義表示,我們又要添加if-else,這太複雜了。 

使用策略模式來解決if-else的問題

是的,就是使用策略模式來解決進行太多的狀態判斷代碼就是一個好辦法。比如,就上邊每一個if-else中的代碼抽成一個類或者方法進行處理。

主要的代碼我就不寫了,因爲下邊纔是我們的主菜,這裏說的這種方式只能解決if-else裏邊的代碼複雜問題,將代碼進行一定程度上的解耦。但並沒有實質地解決if-else的問題,而且這也是網上大多數的解決辦法。

如果對策略模式不太瞭解的小夥伴,可以看下這篇文章,不看也沒關係,在下邊你會看到怎麼用的。策略模式的學習之道

嘗試使用Spring來配合策略模式

程序設計的一大原則“對擴展開放,對修改關閉”,定義一個接口類,用來查詢不同狀態的訂單列表。如下:


 
public interface OrderService { /** * 查詢對應狀態的訂單列表 * @param zt * @return */ List<Order> getOrderList(String zt); } 

然後根據不同的訂單狀態創建不同的實現類,比如,“待付款”的訂單查詢類如下:

雖然以下的命名方式屬於錯誤示範,但是卻能很好地理解


 
@Service("orderServiceDfk") // 這個命名確實很不友好,但是我相信你能理解哈 public class PendingParymentOrderSeviceImpl implements OrderService { /** * 查詢待付款的訂單列表 * * @param zt * @return */ @Override public List<Order> getOrderList(String zt) { //這裏要利用dao層從數據庫中查詢出來相應的訂單列表 return null; } 

看了這兩個類的代碼,我相信小夥伴們應該能理解了要怎麼做了,就是根據前端傳來不同的zt值,後臺使用不同的類來處理,但是我們可以通過Spring來完全取掉if-else。

我們的controller層代碼如下:


 
@RestController public class OrderController { private String orderServiceBeanNamePrefix = "orderService"; @RequestMapping("getOrderList/{zt}") public List<Order> getOrderList(@PathVariable("zt") String zt) { //獲取對應的處理狀態的bean來處理 //就通過這樣一句代碼,完全解決了if-else的判斷邏輯 OrderService orderService = (OrderService) SpingContext.getBean(orderServiceBeanNamePrefix + zt); List<Order> orderList = orderService.getOrderList(); return orderList; } } 

上邊用了一個工具類,就是從Spring 容器中獲取相應的bean,代碼如下:


 
/** * 微信公衆號 “小魚與Java” * * 原理很簡單,我們寫的類實現這個接口,具體可以查閱Spring生命週期相關內容 * Spring會自動調用其中的setApplicationContext方法,傳入Spring容器上下文 * 我們就在這裏把Spring上下文保存下來 * * @date 2020/5/18 * @auther Lyn4ever */ @Component public class SpingContext implements ApplicationContextAware { private static ApplicationContext applicationContext; /** * 根據name從Spring容器中獲取bean * @param name * @return */ public static Object getBean(String name){ return applicationContext.getBean(name); } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { System.out.println("我保存了Spring上下文"); applicationContext = applicationContext; } } 

總結:

解決if-else的思路就是使用策略模式,針對不同“狀態”的訂單,使用不同的類來處理邏輯,這樣就可以很好地進行了“解耦”操作。但是,如果新增一個“狀態表示 ”,我們就要在主邏輯處添加if-else進行判斷要用哪個類來處理。

 


而解決這個“判斷 ”的中使用的if-else就有很多方法:抽象工廠也是一個不錯的方法。而我們使用Spring的控制反轉同樣也可以很好地解決這個問題。

這麼做的好處如下:

  1. “真正的”解決了與我們業務無關的if-else;
  2. 不用前後端再進行狀態的表示“約定”,之前用0表示“待付款”,1表示 “待發貨”這樣的操作,如果記錯,那一定會有大問題。現在,使用特定的字符串來表示,也就是說,前端直接傳入想要解決這個方案對應的bean,從而少去了“複雜且易出錯的約定”環節。

來看代碼實現

工廠模式

爲了方便我們從 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 方法,各自實現即可。

 

好了,不知道這幾種幹掉if--else的方法學會了沒有,如果大家還有更好的辦法,可以在下方評論區說出你的想法

覺得寫的還不錯並且有幫助的,歡迎點贊+關注,轉發給更多人學習,謝謝

更多技術好文,關注公衆號:Java架構師聯盟,謝謝

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章