根據(jù)不同的應(yīng)用場景與意圖,設(shè)計模式主要分為創(chuàng)建型模式、結(jié)構(gòu)型模式和行為型模式三類。本文主要探索行為型模式中的策略模式如何更好地應(yīng)用于實踐中。
前言
在軟件開發(fā)的過程中,需求的多變性幾乎是不可避免的,而作為一名服務(wù)端開發(fā)人員,我們所設(shè)計的程序應(yīng)盡可能支持從技術(shù)側(cè)能夠快速、穩(wěn)健且低成本地響應(yīng)紛繁多變的業(yè)務(wù)需求,從而推進(jìn)業(yè)務(wù)小步快跑、快速迭代。設(shè)計模式正是前輩們針對不同場景下不同類型的問題,所沉淀下來的一套程序設(shè)計思想與解決方案,用來提高代碼可復(fù)用性、可維護(hù)性、可讀性、穩(wěn)健性以及安全性等。下面是設(shè)計模式的祖師爺GoF(Gang of Four,四人幫)的合影,感受一下大佬的氣質(zhì)~
靈活應(yīng)用設(shè)計模式不僅可以使程序本身具有更好的健壯性、易修改性和可擴(kuò)展性,同時它使得編程變得工程化,對于多人協(xié)作的大型項目,能夠降低維護(hù)成本、提升多人協(xié)作效率。根據(jù)不同的應(yīng)用場景與意圖,設(shè)計模式主要分為三類,分別為創(chuàng)建型模式、結(jié)構(gòu)型模式和行為型模式。本文主要探索行為型模式中的策略模式如何更好地應(yīng)用于實踐中。
使用場景
策略模式屬于對象的行為模式,其用意是針對一組可替換的算法,將每一個算法封裝到具有共同接口的獨(dú)立的類中,使得算法可以在不影響到客戶端(算法的調(diào)用方)的情況下發(fā)生變化,使用策略模式可以將算法的定義與使用隔離開來,保證類的單一職責(zé)原則,使得程序整體符合開閉原則。
以手淘中商詳頁的店鋪卡片為例,店鋪卡片主要包含店鋪名稱、店鋪logo、店鋪類型以及店鋪等級等信息,其中不同店鋪類型的店鋪等級計算邏輯是不同的,為了獲取店鋪等級,可以采用如下所示代碼:
if (Objects.equals("淘寶", shopType)) { // 淘寶店鋪等級計算邏輯 // return 店鋪等級; } else if (Objects.equals("天貓", shopType)) { // 天貓店鋪等級計算邏輯 // return 店鋪等級 } else if (Objects.equals("淘特", shopType)) { // 淘特店鋪等級計算邏輯 // return 店鋪等級 } else { // ... }這種寫法雖然實現(xiàn)簡單,但使得各類店鋪等級計算邏輯與程序其他邏輯相耦合,未來如果要對其中一種計算邏輯進(jìn)行更改或者新增加一種計算邏輯,將不得不對原有代碼進(jìn)行更改,違背了OOP的單一職責(zé)原則與開閉原則,讓代碼的維護(hù)變得困難。若項目本身比較復(fù)雜,去改動項目原有的邏輯是一件非常耗時又風(fēng)險巨大的事情。此時我們可以采取策略模式來處理,將不同類型的店鋪等級計算邏輯封裝到具有共同接口又互相獨(dú)立的類中,其核心類圖如下所示: ?
這樣一來,程序便具有了良好的可擴(kuò)展性與易修改性,若想增加一種新的店鋪等級計算邏輯,則可將其對應(yīng)的等級計算邏輯單獨(dú)封裝成ShopRankHandler接口的實現(xiàn)類即可,同樣的,若想對其中一種策略的實現(xiàn)進(jìn)行更改,在相應(yīng)的實現(xiàn)類中進(jìn)行更改即可,而不用侵入原有代碼中去開發(fā)。
最佳實踐探索
本節(jié)仍以店鋪等級的處理邏輯為例,探索策略模式的最佳實踐。當(dāng)使用策略模式的時候,會將一系列算法用具有相同接口的策略類封裝起來,客戶端想調(diào)用某一具體算法,則可分為兩個步驟:1、某一具體策略類對象的獲??;2、調(diào)用策略類中封裝的算法。比如客戶端接受到的店鋪類型為“天貓”,則首先需要獲取TmShopRankHandleImpl類對象,然后調(diào)用其中的算法進(jìn)行天貓店鋪等級的計算。在上述兩個步驟中,步驟2是依賴于步驟1的,當(dāng)步驟1完成之后,步驟2也隨之完成,因此上述步驟1成為整個策略模式中的關(guān)鍵。
下面列舉幾種策略模式的實現(xiàn)方式,其區(qū)別主要在于具體策略類對象獲取的方式不同,對其優(yōu)缺點(diǎn)進(jìn)行分析,并探索其最佳實踐。
?暴力法
店鋪等級計算策略接口
public interface ShopRankHandler { /** * 計算店鋪等級 * @return 店鋪等級 */ public String calculate(); }
各類型店鋪等級計算策略實現(xiàn)類
淘寶店
public class TbShopRankHandleImpl implements ShopRankHandler{ @Override public String calculate() { // 具體計算邏輯 return rank; } }
天貓店
public class TmShopRankHandleImpl implements ShopRankHandler{ @Override public String calculate() { // 具體計算邏輯 return rank; } }
淘特店
public class TtShopRankHandleImpl implements ShopRankHandler{ @Override public String calculate() { // 具體計算邏輯 return rank; } }
客戶端調(diào)用
// 根據(jù)參數(shù)調(diào)用對應(yīng)的算法計算店鋪等級 public String acqurireShopRank(String shopType) { String rank = StringUtil.EMPTY_STRING; if (Objects.equals("淘寶", shopType)) { // 獲取淘寶店鋪等級計算策略類 ShopRankHandler shopRankHandler = new TbShopRankHandleImpl(); // 計算店鋪等級 rank = shopRankHandler.calculate(); } else if (Objects.equals("天貓", shopType)) { // 獲取天貓店鋪等級計算策略類 ShopRankHandler shopRankHandler = new TmShopRankHandleImpl(); // 計算店鋪等級 rank = shopRankHandler.calculate(); } else if (Objects.equals("淘特", shopType)) { // 獲取淘特店鋪等級計算策略類 ShopRankHandler shopRankHandler = new TtShopRankHandleImpl(); // 計算店鋪等級 rank = shopRankHandler.calculate(); } else { // ... } return rank; }
效果
至此,當(dāng)我們需要新增策略類時,需要做的改動如下:
新建策略類并實現(xiàn)策略接口
改動客戶端的if else分支
優(yōu)點(diǎn)
將店鋪等級計算邏輯單獨(dú)進(jìn)行封裝,使其與程序其他邏輯解耦,具有良好的擴(kuò)展性。
實現(xiàn)簡單,易于理解。
缺點(diǎn)
客戶端與策略類仍存在耦合,當(dāng)需要增加一種新類型店鋪時,除了需要增加新的店鋪等級計算策略類,客戶端需要改動if else分支,不符合開閉原則。
?第一次迭代(枚舉+簡單工廠)
有沒有什么方法能使客戶端與具體的策略實現(xiàn)類徹底進(jìn)行解耦,使得客戶端對策略類的擴(kuò)展實現(xiàn)“零”感知?在互聯(lián)網(wǎng)領(lǐng)域,沒有什么問題是加一層解決不了的,我們可以在客戶端與眾多的策略類之間加入工廠來進(jìn)行隔離,使得客戶端只依賴工廠,而具體的策略類由工廠負(fù)責(zé)產(chǎn)生,使得客戶端與策略類解耦,具體實現(xiàn)如下所示:
枚舉類
public enum ShopTypeEnum { TAOBAO("A","淘寶"), TMALL("B", "天貓"), TAOTE("C", "淘特"); @Getter private String type; @Getter private String desc; ShopTypeEnum(String type, String des) { this.type = type; this.desc = des; } }
店鋪等級計算接口
public interface ShopRankHandler { /** * 計算店鋪等級 * @return 店鋪等級 */ String calculate(); }
各類型店鋪等級計算策略實現(xiàn)類
淘寶店
public class TbShopRankHandleImpl implements ShopRankHandler{ @Override public String calculate() { // 具體計算邏輯 return rank; } }
天貓店
public class TmShopRankHandleImpl implements ShopRankHandler{ @Override public String calculate() { // 具體計算邏輯 return rank; } }
淘特店
public class TtShopRankHandleImpl implements ShopRankHandler{ @Override public String calculate() { // 具體計算邏輯 return rank; } }
策略工廠類
@Component public class ShopRankHandlerFactory { // 初始化策略beans private static final MapGET_SHOP_RANK_STRATEGY_MAP = ImmutableMap. builder() .put(ShopTypeEnum.TAOBAO.getType(), new TbShopRankHandleImpl()) .put(ShopTypeEnum.TMALL.getType(), new TmShopRankHandleImpl()) .put(ShopTypeEnum.TAOTE.getType(), new TtShopRankHandleImpl()) ; /** * 根據(jù)店鋪類型獲取對應(yīng)的獲取店鋪卡片實現(xiàn)類 * * @param shopType 店鋪類型 * @return 店鋪類型對應(yīng)的獲取店鋪卡片實現(xiàn)類 */ public ShopRankHandler getStrategy(String shopType) { return GET_SHOP_RANK_STRATEGY_MAP.get(shopType); } }
客戶端調(diào)用
@Resource ShopRankHandlerFactory shopRankHandlerFactory; // 根據(jù)參數(shù)調(diào)用對應(yīng)的算法計算店鋪等級 public String acqurireShopRank(String shopType) { ShopRankHandler shopRankHandler = shopRankHandlerFactory.getStrategy(shopType); return Optional.ofNullable(shopRankHandler) .map(shopRankHandle -> shopRankHandle.calculate()) .orElse(StringUtil.EMPTY_STRING); }
效果
至此,當(dāng)我們需要新增策略類時,需要做的改動如下:
新建策略類并實現(xiàn)策略接口
增加枚舉類型
工廠類中初始化時增加新的策略類
相比上一種方式,策略類與客戶端進(jìn)行解耦,無需更改客戶端的代碼。
優(yōu)點(diǎn)
將客戶端與策略類進(jìn)行解耦,客戶端只面向策略接口進(jìn)行編程,對具體策略類的變化(更改、增刪)完全無感知,符合開閉原則。
缺點(diǎn)
需要引入額外的工廠類,使系統(tǒng)結(jié)構(gòu)變得復(fù)雜。
當(dāng)新加入策略類時,工廠類中初始化策略的部分仍然需要改動。
?第二次迭代(利用Spring框架初始化策略beans)
在枚舉+簡單工廠實現(xiàn)的方式中,利用簡單工廠將客戶端與具體的策略類實現(xiàn)進(jìn)行了解耦,但工廠類中初始化策略beans的部分仍然與具體策略類存在耦合,為了進(jìn)一步解耦,我們可以利用Spring框架中的InitializingBean接口與ApplicationContextAware接口來實現(xiàn)策略beans的自動裝配。InitializingBean接口中的afterPropertiesSet()方法在類的實例化過程當(dāng)中執(zhí)行,也就是說,當(dāng)客戶端完成注入ShopRankHandlerFactory工廠類實例的時候,afterPropertiesSet()也已經(jīng)執(zhí)行完成。因此我們可以通過重寫afterPropertiesSet()方法,在其中利用getBeansOfType()方法來獲取到策略接口的所有實現(xiàn)類,并存于Map容器之中,達(dá)到工廠類與具體的策略類解耦的目的。相比于上一種實現(xiàn)方式,需要改動的代碼如下:
店鋪等級計算接口
public interface ShopRankHandler { /** * 獲取店鋪類型的方法,接口的實現(xiàn)類需要根據(jù)各自的枚舉類型來實現(xiàn),后面就不貼出實現(xiàn)類的代碼 * @return 店鋪等級 */ String getType(); /** * 計算店鋪等級 * @return 店鋪等級 */ String calculate(); }
策略工廠類
@Component public class ShopRankHandlerFactory implements InitializingBean, ApplicationContextAware { private ApplicationContext applicationContext; /** * 策略實例容器 */ private MapGET_SHOP_RANK_STRATEGY_MAP; /** * 根據(jù)店鋪類型獲取對應(yīng)的獲取店鋪卡片實現(xiàn)類 * * @param shopType 店鋪類型 * @return 店鋪類型對應(yīng)的獲取店鋪卡片實現(xiàn)類 */ public ShopRankHandler getStrategy(String shopType) { return GET_SHOP_RANK_STRATEGY_MAP.get(shopType); } @Override public void afterPropertiesSet() { Map beansOfType = applicationContext.getBeansOfType(ShopRankHandler.class); GET_SHOP_RANK_STRATEGY_MAP = Optional.ofNullable(beansOfType) .map(beansOfTypeMap -> beansOfTypeMap.values().stream() .filter(shopRankHandle -> StringUtils.isNotEmpty(shopRankHandle.getType())) .collect(Collectors.toMap(ShopRankHandler::getType, Function.identity()))) .orElse(new HashMap<>(8)); } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } }
效果
至此,當(dāng)我們需要新增策略類時,需要做的改動如下:
新建策略類并實現(xiàn)策略接口
增加枚舉類型
相比于上一種方式,可以省略工廠類在初始化策略beans時要增加新的策略類這一步驟。
優(yōu)點(diǎn)
借助Spring框架完成策略beans的自動裝配,使得策略工廠類與具體的策略類進(jìn)一步解耦。
缺點(diǎn)
需要借助Spring框架來完成,不過在Spring框架應(yīng)用如此廣泛的今天,這個缺點(diǎn)可以忽略不計。
?最終迭代(利用泛型進(jìn)一步提高策略工廠復(fù)用性)
經(jīng)過上面兩次迭代以后,策略模式的實現(xiàn)已經(jīng)變得非常方便,當(dāng)需求發(fā)生改變的時候,我們再也不用手忙腳亂了,只需要關(guān)注新增或者變化的策略類就好,而不用侵入原有邏輯去開發(fā)。但是還有沒有改進(jìn)的空間呢?
設(shè)想一下有一個新業(yè)務(wù)同樣需要策略模式來實現(xiàn),如果為其重新寫一個策略工廠類,整個策略工廠類中除了新的策略接口外,其他代碼均與之前的策略工廠相同,出現(xiàn)了大量重復(fù)代碼,這是我們所不能忍受的。為了最大程度避免重復(fù)代碼的出現(xiàn),我們可以使用泛型將策略工廠類中的策略接口參數(shù)化,使其變得更靈活,從而提高其的復(fù)用性。
理論存在,實踐開始!代碼示意如下:
定義泛型接口
public interface GenericInterface{ E getType(); }
定義策略接口繼承泛型接口
public interface StrategyInterfaceA extends GenericInterface{ String handle(); } public interface StrategyInterfaceB extends GenericInterface { String handle(); } public interface StrategyInterfaceC extends GenericInterface { String handle(); }
實現(xiàn)泛型策略工廠
public class HandlerFactory> implements InitializingBean, ApplicationContextAware { private ApplicationContext applicationContext; /** * 泛型策略接口類型 */ private Class strategyInterfaceType; /** * java泛型只存在于編譯期,無法通過例如T.class的方式在運(yùn)行時獲取其類信息 * 因此利用構(gòu)造函數(shù)傳入具體的策略類型class對象為getBeansOfType()方法 * 提供參數(shù) * * @param strategyInterfaceType 要傳入的策略接口類型 */ public HandlerFactory(Class strategyInterfaceType) { this.strategyInterfaceType = strategyInterfaceType; } /** * 策略實例容器 */ private Map GET_SHOP_RANK_STRATEGY_MAP; /** * 根據(jù)不同參數(shù)類型獲取對應(yīng)的接口實現(xiàn)類 * * @param type 參數(shù)類型 * @return 參數(shù)類型對應(yīng)的接口實現(xiàn)類 */ public T getStrategy(E type) { return GET_SHOP_RANK_STRATEGY_MAP.get(type); } @Override public void afterPropertiesSet() { Map beansOfType = applicationContext.getBeansOfType(strategyInterfaceType); System.out.println(beansOfType); GET_SHOP_RANK_STRATEGY_MAP = Optional.ofNullable(beansOfType) .map(beansOfTypeMap -> beansOfTypeMap.values().stream() .filter(strategy -> StringUtils.isNotEmpty(strategy.getType().toString())) .collect(Collectors.toMap(strategy -> strategy.getType(), Function.identity()))) .orElse(new HashMap<>(8)); System.out.println(GET_SHOP_RANK_STRATEGY_MAP); } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } }
有了上述泛型策略工廠類,當(dāng)我們需要新建一個策略工廠類的時候,只需要利用其構(gòu)造函數(shù)傳入相應(yīng)的策略接口即可。生成StrategyInterfaceA、StrategyInterfaceB與StrategyInterfaceC接口的策略工廠如下:
public class BeanConfig { @Bean public HandlerFactorystrategyInterfaceAFactory(){ return new HandlerFactory<>(StrategyInterfaceA.class); } @Bean public HandlerFactory strategyInterfaceBFactory(){ return new HandlerFactory<>(StrategyInterfaceB.class); } @Bean public HandlerFactory strategyInterfaceCFactory(){ return new HandlerFactory<>(StrategyInterfaceC.class); } }
效果
此時,若想新建一個策略工廠,則只需將策略接口作為參數(shù)傳入泛型策略工廠即可,無需再寫重復(fù)的樣板代碼,策略工廠的復(fù)用性大大提高,也大大提高了我們的開發(fā)效率。
優(yōu)點(diǎn)
將策略接口類型參數(shù)化,策略工廠不受接口類型限制,成為任意接口的策略工廠。
缺點(diǎn)
系統(tǒng)的抽象程度、復(fù)雜度變高,不利于直觀理解。
結(jié)束語
學(xué)習(xí)設(shè)計模式,關(guān)鍵是學(xué)習(xí)設(shè)計思想,不能簡單地生搬硬套,靈活正確地應(yīng)用設(shè)計模式可以讓我們在開發(fā)中取得事半功倍的效果,但也不能為了使用設(shè)計模式而過度設(shè)計,要合理平衡設(shè)計的復(fù)雜度和靈活性。
本文是對策略模式最佳實踐的一次探索,不一定是事實上的最佳實踐,歡迎大家指正與討論。
審核編輯:湯梓紅
-
接口
+關(guān)注
關(guān)注
33文章
8496瀏覽量
150834 -
設(shè)計模式
+關(guān)注
關(guān)注
0文章
53瀏覽量
8622
原文標(biāo)題:設(shè)計模式最佳實踐探索—策略模式
文章出處:【微信號:OSC開源社區(qū),微信公眾號:OSC開源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論