業(yè)務(wù)復(fù)雜=if else?剛來(lái)的大神竟然用策略+工廠徹底干掉了他們!
對(duì)于業(yè)務(wù)開(kāi)發(fā)來(lái)說(shuō),業(yè)務(wù)邏輯的復(fù)雜是必然的,隨著業(yè)務(wù)發(fā)展,需求只會(huì)越來(lái)越復(fù)雜,為了考慮到各種各樣的情況,代碼中不可避免的會(huì)出現(xiàn)很多if-else。
一旦代碼中if-else過(guò)多,就會(huì)大大的影響其可讀性和可維護(hù)性。
首先可讀性,不言而喻,過(guò)多的if-else代碼和嵌套,會(huì)使閱讀代碼的人很難理解到底是什么意思。尤其是那些沒(méi)有注釋的代碼。
其次是可維護(hù)性,因?yàn)閕f-else特別多,想要新加一個(gè)分支的時(shí)候,就會(huì)很難添加,極其容易影響到其他的分支。
筆者曾經(jīng)看到過(guò)一個(gè)支付的核心應(yīng)用,這個(gè)應(yīng)用支持了很多業(yè)務(wù)的線上支付功能,但是每個(gè)業(yè)務(wù)都有很多定制的需求,所以很多核心的代碼中都有一大坨if-else。
每個(gè)新業(yè)務(wù)需要定制的時(shí)候,都把自己的if放到整個(gè)方法的最前面,以保證自己的邏輯可以正常執(zhí)行。這種做法,后果可想而知。
其實(shí),if-else是有辦法可以消除掉的,其中比較典型的并且使用廣泛的就是借助策略模式和工廠模式,準(zhǔn)確的說(shuō)是利用這兩個(gè)設(shè)計(jì)模式的思想,徹底消滅代碼中的if-else。
本文,就結(jié)合這兩種設(shè)計(jì)模式,介紹如何消除if-else,并且,還會(huì)介紹如何和Spring框架結(jié)合,這樣讀者看完本文之后就可以立即應(yīng)用到自己的項(xiàng)目中。
本文涉及到一些代碼,但是作者盡量用通俗的例子和偽代碼等形式使內(nèi)容不那么枯燥。
惡心的if-else
假設(shè)我們要做一個(gè)外賣(mài)平臺(tái),有這樣的需求:
1、外賣(mài)平臺(tái)上的某家店鋪為了促銷,設(shè)置了多種會(huì)員優(yōu)惠,其中包含超級(jí)會(huì)員折扣8折、普通會(huì)員折扣9折和普通用戶沒(méi)有折扣三種。
2、希望用戶在付款的時(shí)候,根據(jù)用戶的會(huì)員等級(jí),就可以知道用戶符合哪種折扣策略,進(jìn)而進(jìn)行打折,計(jì)算出應(yīng)付金額。
3、隨著業(yè)務(wù)發(fā)展,新的需求要求專屬會(huì)員要在店鋪下單金額大于30元的時(shí)候才可以享受優(yōu)惠。
4、接著,又有一個(gè)變態(tài)的需求,如果用戶的超級(jí)會(huì)員已經(jīng)到期了,并且到期時(shí)間在一周內(nèi),那么就對(duì)用戶的單筆訂單按照超級(jí)會(huì)員進(jìn)行折扣,并在收銀臺(tái)進(jìn)行強(qiáng)提醒,引導(dǎo)用戶再次開(kāi)通會(huì)員,而且折扣只進(jìn)行一次。
那么,我們可以看到以下偽代碼:
- public BigDecimal calPrice(BigDecimal orderPrice, String buyerType) {
- if (用戶是專屬會(huì)員) {
- if (訂單金額大于30元) {
- returen 7折價(jià)格;
- }
- }
- if (用戶是超級(jí)會(huì)員) {
- return 8折價(jià)格;
- }
- if (用戶是普通會(huì)員) {
- if(該用戶超級(jí)會(huì)員剛過(guò)期并且尚未使用過(guò)臨時(shí)折扣){
- 臨時(shí)折扣使用次數(shù)更新();
- returen 8折價(jià)格;
- }
- return 9折價(jià)格;
- }
- return 原價(jià);
- }
以上,就是對(duì)于這個(gè)需求的一段價(jià)格計(jì)算邏輯,使用偽代碼都這么復(fù)雜,如果是真的寫(xiě)代碼,那復(fù)雜度可想而知。
這樣的代碼中,有很多if-else,并且還有很多的if-else的嵌套,無(wú)論是可讀性還是可維護(hù)性都非常低。
那么,如何改善呢?
策略模式
接下來(lái),我們嘗試引入策略模式來(lái)提升代碼的可維護(hù)性和可讀性。
首先,定義一個(gè)接口:
- /**
- * @author mhcoding
- */
- public interface UserPayService {
- /**
- * 計(jì)算應(yīng)付價(jià)格
- */
- public BigDecimal quote(BigDecimal orderPrice);
- }
接著定義幾個(gè)策略類:
- /**
- * @author mhcoding
- */
- public class ParticularlyVipPayService implements UserPayService {
- @Override
- public BigDecimal quote(BigDecimal orderPrice) {
- if (消費(fèi)金額大于30元) {
- return 7折價(jià)格;
- }
- }
- }
- public class SuperVipPayService implements UserPayService {
- @Override
- public BigDecimal quote(BigDecimal orderPrice) {
- return 8折價(jià)格;
- }
- }
- public class VipPayService implements UserPayService {
- @Override
- public BigDecimal quote(BigDecimal orderPrice) {
- if(該用戶超級(jí)會(huì)員剛過(guò)期并且尚未使用過(guò)臨時(shí)折扣){
- 臨時(shí)折扣使用次數(shù)更新();
- returen 8折價(jià)格;
- }
- return 9折價(jià)格;
- }
- }
引入了策略之后,我們可以按照如下方式進(jìn)行價(jià)格計(jì)算:
- /**
- * @author mhcoding
- */
- public class Test {
- public static void main(String[] args) {
- UserPayService strategy = new VipPayService();
- BigDecimal quote = strategy.quote(300);
- System.out.println("普通會(huì)員商品的最終價(jià)格為:" + quote.doubleValue());
- strategy = new SuperVipPayService();
- quote = strategy.quote(300);
- System.out.println("超級(jí)會(huì)員商品的最終價(jià)格為:" + quote.doubleValue());
- }
- }
以上,就是一個(gè)例子,可以在代碼中new出不同的會(huì)員的策略類,然后執(zhí)行對(duì)應(yīng)的計(jì)算價(jià)格的方法。這個(gè)例子以及策略模式的相關(guān)知識(shí),讀者可以在《如何給女朋友解釋什么是策略模式?》一文中學(xué)習(xí)。
但是,真正在代碼中使用,比如在一個(gè)web項(xiàng)目中使用,上面這個(gè)Demo根本沒(méi)辦法直接用。
首先,在web項(xiàng)目中,上面我們創(chuàng)建出來(lái)的這些策略類都是被Spring托管的,我們不會(huì)自己去new一個(gè)實(shí)例出來(lái)。
其次,在web項(xiàng)目中,如果真要計(jì)算價(jià)格,也是要事先知道用戶的會(huì)員等級(jí),比如從數(shù)據(jù)庫(kù)中查出會(huì)員等級(jí),然后根據(jù)等級(jí)獲取不同的策略類執(zhí)行計(jì)算價(jià)格方法。
那么,web項(xiàng)目中真正的計(jì)算價(jià)格的話,偽代碼應(yīng)該是這樣的:
- /**
- * @author mhcoding
- */
- public BigDecimal calPrice(BigDecimal orderPrice,User user) {
- String vipType = user.getVipType();
- if (vipType == 專屬會(huì)員) {
- //偽代碼:從Spring中獲取超級(jí)會(huì)員的策略對(duì)象
- UserPayService strategy = Spring.getBean(ParticularlyVipPayService.class);
- return strategy.quote(orderPrice);
- }
- if (vipType == 超級(jí)會(huì)員) {
- UserPayService strategy = Spring.getBean(SuperVipPayService.class);
- return strategy.quote(orderPrice);
- }
- if (vipType == 普通會(huì)員) {
- UserPayService strategy = Spring.getBean(VipPayService.class);
- return strategy.quote(orderPrice);
- }
- return 原價(jià);
- }
通過(guò)以上代碼,我們發(fā)現(xiàn),代碼可維護(hù)性和可讀性好像是好了一些,但是好像并沒(méi)有減少if-else啊。
其實(shí),在之前的《如何給女朋友解釋什么是策略模式?》一文中,我們介紹了很多策略模式的優(yōu)點(diǎn)。但是,策略模式的使用上,還是有一個(gè)比較大的缺點(diǎn)的:
客戶端必須知道所有的策略類,并自行決定使用哪一個(gè)策略類。這就意味著客戶端必須理解這些算法的區(qū)別,以便適時(shí)選擇恰當(dāng)?shù)乃惴悺?/p>
也就是說(shuō),雖然在計(jì)算價(jià)格的時(shí)候沒(méi)有if-else了,但是選擇具體的策略的時(shí)候還是不可避免的還是要有一些if-else。
另外,上面的偽代碼中,從Spring中獲取會(huì)員的策略對(duì)象我們是偽代碼實(shí)現(xiàn)的,那么代碼到底該如何獲取對(duì)應(yīng)的Bean呢?
接下來(lái)我們看如何借助Spring和工廠模式,解決上面這些問(wèn)題。
工廠模式
為了方便我們從Spring中獲取UserPayService的各個(gè)策略類,我們創(chuàng)建一個(gè)工廠類:
- /**
- * @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);
- }
- }
這個(gè)UserPayServiceStrategyFactory中定義了一個(gè)Map,用來(lái)保存所有的策略類的實(shí)例,并提供一個(gè)getByUserType方法,可以根據(jù)類型直接獲取對(duì)應(yīng)的類的實(shí)例。還有一個(gè)register方法,這個(gè)后面再講。
有了這個(gè)工廠類之后,計(jì)算價(jià)格的代碼即可得到大大的優(yōu)化:
- /**
- * @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類型之后,直接通過(guò)工廠的getByUserType方法直接調(diào)用就可以了。
通過(guò)策略+工廠,我們的代碼很大程度的優(yōu)化了,大大提升了可讀性和可維護(hù)性。
但是,上面還遺留了一個(gè)問(wèn)題,那就是UserPayServiceStrategyFactory中用來(lái)保存所有的策略類的實(shí)例的Map是如何被初始化的?各個(gè)策略的實(shí)例對(duì)象如何塞進(jìn)去的呢?
Spring Bean的注冊(cè)
還記得我們前面定義的UserPayServiceStrategyFactory中提供了的register方法嗎?他就是用來(lái)注冊(cè)策略服務(wù)的。
接下來(lái),我們就想辦法調(diào)用register方法,把Spring通過(guò)IOC創(chuàng)建出來(lái)的Bean注冊(cè)進(jìn)去就行了。
這種需求,可以借用Spring種提供的InitializingBean接口,這個(gè)接口為Bean提供了屬性初始化后的處理方法,它只包括afterPropertiesSet方法,凡是繼承該接口的類,在bean的屬性初始化后都會(huì)執(zhí)行該方法。
那么,我們將前面的各個(gè)策略類稍作改造即可:
- /**
- * @author mhcoding
- */
- @Service
- public class ParticularlyVipPayService implements UserPayService,InitializingBean {
- @Override
- public BigDecimal quote(BigDecimal orderPrice) {
- if (消費(fèi)金額大于30元) {
- return 7折價(jià)格;
- }
- }
- @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折價(jià)格;
- }
- @Override
- public void afterPropertiesSet() throws Exception {
- UserPayServiceStrategyFactory.register("SuperVip",this);
- }
- }
- @Service
- public class VipPayService implements UserPayService,InitializingBean {
- @Override
- public BigDecimal quote(BigDecimal orderPrice) {
- if(該用戶超級(jí)會(huì)員剛過(guò)期并且尚未使用過(guò)臨時(shí)折扣){
- 臨時(shí)折扣使用次數(shù)更新();
- returen 8折價(jià)格;
- }
- return 9折價(jià)格;
- }
- @Override
- public void afterPropertiesSet() throws Exception {
- UserPayServiceStrategyFactory.register("Vip",this);
- }
- }
只需要每一個(gè)策略服務(wù)的實(shí)現(xiàn)類都實(shí)現(xiàn)InitializingBean接口,并實(shí)現(xiàn)其afterPropertiesSet方法,在這個(gè)方法中調(diào)用UserPayServiceStrategyFactory.register即可。
這樣,在Spring初始化的時(shí)候,當(dāng)創(chuàng)建VipPayService、SuperVipPayService和ParticularlyVipPayService的時(shí)候,會(huì)在Bean的屬性初始化之后,把這個(gè)Bean注冊(cè)到UserPayServiceStrategyFactory中。
以上代碼,其實(shí)還是有一些重復(fù)代碼的,這里面還可以引入模板方法模式進(jìn)一步精簡(jiǎn),這里就不展開(kāi)了。
還有就是,UserPayServiceStrategyFactory.register調(diào)用的時(shí)候,第一個(gè)參數(shù)需要傳一個(gè)字符串,這里的話其實(shí)也可以優(yōu)化掉。比如使用枚舉,或者在每個(gè)策略類中自定義一個(gè)getUserType方法,各自實(shí)現(xiàn)即可。
總結(jié)
本文,我們通過(guò)策略模式、工廠模式以及Spring的InitializingBean,提升了代碼的可讀性以及可維護(hù)性,徹底消滅了一坨if-else。
文中的這種做法,大家可以立刻嘗試起來(lái),這種實(shí)踐,是我們?nèi)粘i_(kāi)發(fā)中經(jīng)常用到的,而且還有很多衍生的用法,也都非常好用。有機(jī)會(huì)后面再介紹。
其實(shí),如果讀者們對(duì)策略模式和工廠模式了解的話,文中使用的并不是嚴(yán)格意義上面的策略模式和工廠模式。
首先,策略模式中重要的Context角色在這里面是沒(méi)有的,沒(méi)有Context,也就沒(méi)有用到組合的方式,而是使用工廠代替了。
另外,這里面的UserPayServiceStrategyFactory其實(shí)只是維護(hù)了一個(gè)Map,并提供了register和get方法而已,而工廠模式其實(shí)是幫忙創(chuàng)建對(duì)象的,這里并沒(méi)有用到。
所以,讀者不必糾結(jié)于到底是不是真的用了策略模式和工廠模式。而且,這里面也再擴(kuò)展一句,所謂的GOF 23種設(shè)計(jì)模式,無(wú)論從哪本書(shū)或者哪個(gè)博客看,都是簡(jiǎn)單的代碼示例,但是我們?nèi)粘i_(kāi)發(fā)很多都是基于Spring等框架的,根本沒(méi)辦法直接用的。
所以,對(duì)于設(shè)計(jì)模式的學(xué)習(xí),重要的是學(xué)習(xí)其思想,而不是代碼實(shí)現(xiàn)!!!
如果讀者們感興趣,后續(xù)可以出更多的設(shè)計(jì)模式和Spring等框架結(jié)合使用的最佳實(shí)踐。希望通過(guò)這樣的文章,讀者可以真正的在代碼中使用上設(shè)計(jì)模式。