剛來(lái)的大神徹底干掉了代碼中的if else...
對(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)?if-else 特別多,想要新加一個(gè)分支的時(shí)候,就會(huì)很難添加,極其容易影響到其他的分支。
筆者曾經(jīng)看到過(guò)一個(gè)支付的核心應(yīng)用,這個(gè)應(yīng)用支持了很多業(yè)務(wù)的線(xiàn)上支付功能,但是每個(gè)業(yè)務(wù)都有很多定制的需求,所以很多核心的代碼中都有一大坨 if-else。
每個(gè)新業(yè)務(wù)需要定制的時(shí)候,都把自己的 if 放到整個(gè)方法的最前面,以保證自己的邏輯可以正常執(zhí)行。這種做法,后果可想而知。
其實(shí),if-else 是有辦法可以消除掉的,其中比較典型的并且使用廣泛的就是借助策略模式和工廠(chǎng)模式,準(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),有這樣的需求:
- 外賣(mài)平臺(tái)上的某家店鋪為了促銷(xiāo),設(shè)置了多種會(huì)員優(yōu)惠,其中包含超級(jí)會(huì)員折扣 8 折、普通會(huì)員折扣 9 折和普通用戶(hù)沒(méi)有折扣三種。
- 希望用戶(hù)在付款的時(shí)候,根據(jù)用戶(hù)的會(huì)員等級(jí),就可以知道用戶(hù)符合哪種折扣策略,進(jìn)而進(jìn)行打折,計(jì)算出應(yīng)付金額。
- 隨著業(yè)務(wù)發(fā)展,新的需求要求專(zhuān)屬會(huì)員要在店鋪下單金額大于 30 元的時(shí)候才可以享受優(yōu)惠。
- 接著,又有一個(gè)變態(tài)的需求,如果用戶(hù)的超級(jí)會(huì)員已經(jīng)到期了,并且到期時(shí)間在一周內(nèi),那么就對(duì)用戶(hù)的單筆訂單按照超級(jí)會(huì)員進(jìn)行折扣,并在收銀臺(tái)進(jìn)行強(qiáng)提醒,引導(dǎo)用戶(hù)再次開(kāi)通會(huì)員,而且折扣只進(jìn)行一次。
那么,我們可以看到以下偽代碼:
- public BigDecimal calPrice(BigDecimal orderPrice, String buyerType) {
- if (用戶(hù)是專(zhuān)屬會(huì)員) {
- if (訂單金額大于30元) {
- returen 7折價(jià)格;
- }
- }
- if (用戶(hù)是超級(jí)會(huì)員) {
- return 8折價(jià)格;
- }
- if (用戶(hù)是普通會(huì)員) {
- if(該用戶(hù)超級(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è)策略類(lèi):
- /**
- * @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(該用戶(hù)超級(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ì)員的策略類(lèi),然后執(zhí)行對(duì)應(yīng)的計(jì)算價(jià)格的方法。
但是,真正在代碼中使用,比如在一個(gè) Web 項(xiàng)目中使用,上面這個(gè) Demo 根本沒(méi)辦法直接用。
首先,在 Web 項(xiàng)目中,上面我們創(chuàng)建出來(lái)的這些策略類(lèi)都是被 Spring 托管的,我們不會(huì)自己去 New 一個(gè)實(shí)例出來(lái)。
其次,在 Web 項(xiàng)目中,如果真要計(jì)算價(jià)格,也是要事先知道用戶(hù)的會(huì)員等級(jí),比如從數(shù)據(jù)庫(kù)中查出會(huì)員等級(jí),然后根據(jù)等級(jí)獲取不同的策略類(lèi)執(zhí)行計(jì)算價(jià)格方法。
那么,Web 項(xiàng)目中真正的計(jì)算價(jià)格的話(huà),偽代碼應(yīng)該是這樣的:
- /**
- * @author mhcoding
- */
- public BigDecimal calPrice(BigDecimal orderPrice,User user) {
- String vipType = user.getVipType();
- if (vipType == 專(zhuān)屬會(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 啊。
但是,策略模式的使用上,還是有一個(gè)比較大的缺點(diǎn)的:客戶(hù)端必須知道所有的策略類(lèi),并自行決定使用哪一個(gè)策略類(lèi)。這就意味著客戶(hù)端必須理解這些算法的區(qū)別,以便適時(shí)選擇恰當(dāng)?shù)乃惴?lèi)。
也就是說(shuō),雖然在計(jì)算價(jià)格的時(shí)候沒(méi)有 if-else 了,但是選擇具體的策略的時(shí)候還是不可避免的還是要有一些 if-else。
另外,上面的偽代碼中,從 Spring 中獲取會(huì)員的策略對(duì)象我們是偽代碼實(shí)現(xiàn)的,那么代碼到底該如何獲取對(duì)應(yīng)的 Bean 呢?
接下來(lái)我們看如何借助 Spring 和工廠(chǎng)模式,解決上面這些問(wèn)題。
工廠(chǎng)模式
為了方便我們從 Spring 中獲取 UserPayService 的各個(gè)策略類(lèi),我們創(chuàng)建一個(gè)工廠(chǎng)類(lèi):
- /**
- * @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)保存所有的策略類(lèi)的實(shí)例,并提供一個(gè) getByUserType 方法,可以根據(jù)類(lèi)型直接獲取對(duì)應(yīng)的類(lèi)的實(shí)例。還有一個(gè) Register 方法,這個(gè)后面再講。
有了這個(gè)工廠(chǎng)類(lèi)之后,計(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 了,拿到用戶(hù)的 vip 類(lèi)型之后,直接通過(guò)工廠(chǎng)的 getByUserType 方法直接調(diào)用就可以了。
通過(guò)策略+工廠(chǎng),我們的代碼很大程度的優(yōu)化了,大大提升了可讀性和可維護(hù)性。
但是,上面還遺留了一個(gè)問(wèn)題,那就是 UserPayServiceStrategyFactory 中用來(lái)保存所有的策略類(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 方法,凡是繼承該接口的類(lèi),在 Bean 的屬性初始化后都會(huì)執(zhí)行該方法。
那么,我們將前面的各個(gè)策略類(lèi)稍作改造即可:
- /**
- * @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(該用戶(hù)超級(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)類(lèi)都實(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è)字符串,這里的話(huà)其實(shí)也可以?xún)?yōu)化掉。
比如使用枚舉,或者在每個(gè)策略類(lèi)中自定義一個(gè) getUserType 方法,各自實(shí)現(xiàn)即可。
總結(jié)
本文,我們通過(guò)策略模式、工廠(chǎng)模式以及 Spring 的 InitializingBean,提升了代碼的可讀性以及可維護(hù)性,徹底消滅了一坨 if-else。
文中的這種做法,大家可以立刻嘗試起來(lái),這種實(shí)踐,是我們?nèi)粘i_(kāi)發(fā)中經(jīng)常用到的,而且還有很多衍生的用法,也都非常好用。有機(jī)會(huì)后面再介紹。
其實(shí),如果讀者們對(duì)策略模式和工廠(chǎng)模式了解的話(huà),文中使用的并不是嚴(yán)格意義上面的策略模式和工廠(chǎng)模式。
首先,策略模式中重要的 Context 角色在這里面是沒(méi)有的,沒(méi)有 Context,也就沒(méi)有用到組合的方式,而是使用工廠(chǎng)代替了。
另外,這里面的 UserPayServiceStrategyFactory 其實(shí)只是維護(hù)了一個(gè) Map,并提供了 Register 和 Get 方法而已,而工廠(chǎng)模式其實(shí)是幫忙創(chuàng)建對(duì)象的,這里并沒(méi)有用到。
所以,讀者不必糾結(jié)于到底是不是真的用了策略模式和工廠(chǎng)模式。而且,這里面也再擴(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)!!!希望通過(guò)這樣的文章,讀者可以真正的在代碼中使用上設(shè)計(jì)模式。