業務複雜=if else?剛來的大神竟然用策略+工廠徹底幹掉了他們!

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

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

業務複雜=if else?剛來的大神竟然用策略+工廠徹底幹掉了他們!

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

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

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

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

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

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

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

噁心的if-else

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

1、外賣平臺上的某家店鋪為了促銷,設定了多種會員優惠,其中包含超級會員折扣8折、普通會員折扣9折和普通使用者沒有折扣三種。

2、希望使用者在付款的時候,根據使用者的會員等級,就可以知道使用者符合哪種折扣策略,進而進行打折,計算出應付金額。

3、隨著業務發展,新的需求要求專屬會員要在店鋪下單金額大於30元的時候才可以享受優惠。

4、接著,又有一個變態的需求,如果使用者的超級會員已經到期了,並且到期時間在一週內,那麼就對使用者的單筆訂單按照超級會員進行折扣,並在收銀臺進行強提醒,引導使用者再次開通會員,而且折扣只進行一次。

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

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等框架的,根本沒辦法直接用的。

所以,對於設計模式的學習,重要的是學習其思想,而不是程式碼實現!!!

如果讀者們感興趣,後續可以出更多的設計模式和Spring等框架結合使用的最佳實踐。希望通過這樣的文章,讀者可以真正的在程式碼中使用上設計模式。

業務複雜=if else?剛來的大神竟然用策略+工廠徹底幹掉了他們!


相關文章