掌握設計模式之策略模式
- 2019 年 10 月 28 日
- 筆記
前言
最近段時間,接到一個需求:開發一個聚合支付服務,對其他內部項目提供統一的介面來實現不同支付平台的支付能力發起,比如支付寶,微信,銀聯等。為了處理相似的支付操作而各平台具體實現不同的情況,要讓各個平台介面能力能相互獨立,並要方便擴展後續新增的支付平台,我引入了設計模式的策略模式來應對需求場景,藉此深入學習總結下策略模式,於是也就有了本文,希望對學習策略模式的同學有所幫助。
為什麼需要策略模式
日常工作開發中我們總會遇到如下熟悉的程式碼片段:
if(condition1){ // do something1 } else if (condition2){ // do something2 } else if (condition3){ // do something3 }
在每個 if 條件下都有數十行甚至百行的業務處理,各自處理又是相互獨立的並且目的一致,都匯聚在一個方法里。這樣的寫法不但讓類變得臃腫冗長,並且不同邏輯都在一個類中修改,維護和擴展起來都很費勁。那麼又有什麼辦法可以優化這大段的程式碼呢,在實現功能的同時,讓程式碼更加靈活和易維護。
要解決這個問題,本文的主角—策略模式 就登場了,作為設計模式中比較簡單的行為型模式,其實很多框架中都見到它的身影,稍後我們也會從各框架源碼中識別策略模式的應用。使用策略模式可以幫助我們將每個處理邏輯封裝成獨立的類,客戶端類需要進行哪種處理邏輯就使用對應的類,調用其封裝了業務處理細節的方法即可。這樣一來,客戶端類減少了業務處理邏輯的大量程式碼,讓自身更加精簡。當業務邏輯有所改動時,只要在對應的類中修改,而不影響其他的類;並且如果出現了新的業務邏輯只要新增相似的類進行實現,供客戶端類調用即可。
什麼是策略模式
接下來我們就介紹下策略模式的定義和組成,以及它的基本形式。
首先看下維基百科上策略模式的定義:
In computer programming, the strategy pattern (also known as the policy pattern) is a behavioral software design pattern that enables selecting an algorithm at runtime. Instead of implementing a single algorithm directly, code receives run-time instructions as to which in a family of algorithms to use.
策略模式也叫政策模式,允許程式在運行時選擇一個演算法執行,通常存在一類演算法實現提供外部選擇執行,這裡的演算法,也可以叫做策略,相當於上節內容提到的具體處理邏輯。
再來看下 《設計模式:可復用面向對象軟體的基礎》一書中對策略模式的定義:
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from the clients that use it.
再次對其定義解讀:定義一類演算法,各自獨立封裝實現,並且相互之間是可替換的。除此之外,由客戶端類決定具體使用哪個演算法。
上述兩個定義都提到了演算法一詞,它表示了完整的,不可再拆分的業務邏輯處理。通常用介面或者抽象類來表示一類演算法的抽象,提供多種對該類演算法的操作實現,以此組成一類獨立且可替換的演算法,也叫策略組。
了解完定義後,我們再來看下策略模式通用類圖:
類圖中涉及三類角色:Context,Strategy 和 ConcreteStrategy
- Strategy:抽象策略角色,代表某個演算法的介面或者抽象類,定義了每個演算法或者策略需要具有的方法和屬性。
- Context:上下文角色,引用策略介面對象,屏蔽了外部模組對策略或方法的直接訪問,只能通過Context 提供的方法訪問。
- ConcreteStrategy:抽象策略的具體實現,該類含有具體的演算法,並且通常不只一種實現,有多個類。
這三個角色的功能職責都十分明確,對應的源碼實現也十分簡單,現在我們就來快速看下每個角色對應的通用源碼。
// 抽象的策略角色 public interface Strategy { void doSomething(); } // 具體策略角色 public class ConcreteStrategy implements Strategy { @Override public void doSomething() { System.out.println("ConcreteStrategy doSomething !"); } } // 上下文角色 public class Context { private final Strategy strategy; public Context(Strategy strategy) { this.strategy = strategy; } public void doAnything() { this.strategy.doSomething(); } }
有了策略模式的基本程式碼結構,在客戶端類中使用十分簡單,想要哪個策略,就產生出它的具體策略對象放入上下文對象內,然後由上下文對象執行具體策略操作即可,具體程式碼如下:
public class Client { public static void main(String[] args) { Strategy strategy = new ConcreteStrategy(); Context context = new Context(strategy); context.doAnything(); // ConcreteStrategy doSomething ! } }
識別策略模式
看清楚了策略模式的定義,角色組成以及通用的程式碼結構之後,我們就來看下策略模式在通用框架里的應用,來加深對策略模式的認識。
JDK 與策略模式
在常用的Java 集合框架中,比較器 java.util.Comparator 的設計就採用了策略模式。Comparator 就是一個抽象的策略介面,只要一個類實現這個介面,自定 compare 方法,該類成為具體策略類,你可以在很多地址找到這個抽象策略介面的實現,官方在工具類 java.util.Comparators 里也提供 NaturalOrderComparator,NullComparator 兩種具體策略類。而使用 Comparator 到的 java.util.Collections 類就是 Context 角色,將集合的比較功能封裝成靜態方法對外提供。
Spring Framework 與策略模式
Spring 框架最早以 IoC 和 DI 兩大特性著稱,不需要開發者自己創建對象,而是通過 Spring IoC 容器識別然後實例化所需對象。在 Spring 中將執行創建對象實例的這個操作封裝為一種演算法,用介面類 org.springframework.beans.factory.support.InstantiationStrategy 進行聲明,而具體策略類則有 org.springframework.beans.factory.support.SimpleInstantiationStrategy 和 org.springframework.beans.factory.support.CglibSubclassingInstantiationStrategy 兩個,並且 CglibSubclassingInstantiationStrategy 是對 SimpleInstantiationStrategy 的繼承擴展,也是 Spring 容器中真正使用到的策略類,具體應用的源碼可參考 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory 類:
/** * Instantiate the given bean using its default constructor. * @param beanName the name of the bean * @param mbd the bean definition for the bean * @return a BeanWrapper for the new instance */ protected BeanWrapper instantiateBean(final String beanName, final RootBeanDefinition mbd) { //... beanInstance = getInstantiationStrategy().instantiate(mbd, beanName, parent); //... }
如何使用策略模式
實例應用
俗話說學以致用,接觸了策略模式後我們應該想想怎麼用在自己日常開發項目中呢,這裡就簡單通過一個實例來說明下策略模式的使用方式。假設現在有個需求:需要對一個目錄或者文件實現兩種不同格式的解壓縮方式:zip壓縮和gzip壓縮,也後續可能新增其他的解壓縮方式。
我們首先將解壓縮的演算法抽象成抽象策略介面 CompressStrategy, 提供壓縮方法 compress 和解壓縮方法 uncompress,分別接受源文件路徑和目的文件路徑。
策略類在命名通常上以 Strategy 為後綴,來指明自身採用策略模式進行設計,以此簡化與其他人溝通成本。
public interface CompressStrategy { public boolean compress(String source, String to); public boolean uncompress(String source, String to); }
再對抽象策略介面進行實現,分別提供zip 壓縮演算法和 gzip 壓縮演算法,程式碼如下:
public class ZipStrategy implements CompressStrategy { @Override public boolean compress(String source, String to) { System.out.println(source + " --> " + to + " ZIP壓縮成功!"); return true; } @Override public boolean uncompress(String source, String to) { System.out.println(source + " --> " + to + " ZIP解壓縮成功!"); return true; } } public class GzipStrategy implements CompressStrategy { @Override public boolean compress(String source, String to) { System.out.println(source + " --> " + to + " GZIP壓縮成功!"); return true; } @Override public boolean uncompress(String source, String to) { System.out.println(source + " --> " + to + " GZIP解壓縮成功!"); return true; } }
程式碼示例里的實現為了簡化只是簡單列印操作,具體實現可以參考 JDK API 進行操作。
接下來看下 Context 角色的程式碼實現:
public class CompressContext { private CompressStrategy compressStrategy; public CompressContext(CompressStrategy compressStrategy) { this.compressStrategy = compressStrategy; } public boolean compress(String source, String to) { return compressStrategy.compress(source, to); } public boolean uncompress(String source, String to) { return compressStrategy.uncompress(source, to); } }
十分簡單,只是傳入一個具體演算法,然後執行,到這裡標準的策略模式就編寫完畢了。客戶端類只是根據需要指定的具體壓縮策略對象傳給 CompressContext 對象即可。如果要新增一個壓縮演算法,也只需對 CompressStrategy 介面提供新的實現即可傳給 CompressContext 對象使用。
public class Client { public static void main(String[] args) { CompressContext context; System.out.println("========執行演算法========"); context = new CompressContext(new ZipStrategy()); context.compress("c:\file", "d:\file.zip"); context.uncompress("c:\file.zip", "d:\file"); System.out.println("========切換演算法========"); context = new CompressContext(new GzipStrategy()); context.compress("c:\file", "d:\file.gzip"); context.uncompress("c:\file.gzip", "d:\file"); } }
上面的策略模式的應用示例是不是很簡單,類似應用也有很多,比如要對接第三方支付,不同的支付平台有不同的支付API,這個API操作都可以抽象成策略介面,客戶端發起特定平台的支付介面時,我們只需調用具體的支付策略類執行,並且每個支付策略類相互獨立,可替換。
適用場景
本節最後簡單總結下策略模式的適用場景:
-
如果一個對象有很多的行為,它們的實現目的相同,而這些行為使用了多重的條件選擇語句來實現。
- 當一個系統需要動態地切換演算法,會選擇一種演算法去執行。
-
客戶端類不需要知道具體演算法的實現細節,只要調用並完成所需要求。
Lambda 與 策略模式
JDK 8 之後,利用Lambda可以提供策略模式更加精簡的實現,如果策略介面是一個函數介面,那麼不需要聲明新的類來實現不同策略,直接通過傳遞Lambda就可實現,並且更加簡潔,具體使用方式參見下方程式碼:
/** * Context 對象 */ public class Validator { private final ValidationStrategy strategy; public Validator(ValidationStrategy v) { this.strategy = v; } public boolean validate(String s) { return strategy.execute(s); } } /** * 策略介面 */ @FunctionalInterface public interface ValidationStrategy { boolean execute(String s); } numericValidator = new Validator((String s) -> s.matches("[a-z]+")); b1 = numericValidator.validate("aaaa"); // true lowerCaseValidator = new Validator((String s) -> s.matches("\d+")); b2 = lowerCaseValidator.validate("bbbb"); // false
結合 Lambda 的策略模式更適合用於處理簡單演算法操作的場景,如果演算法實現複雜過於冗長複雜,還是建議拆分成單個類進行實現。
策略模式的注意點
策略模式使用起來雖然簡單,但它的靈活性在許多項目都能見到其身影,在使用時也有需要注意的地方,下面我們就來看下:
-
策略模式中每個演算法都是完整,不可拆分的原子業務,並且多個演算法必須是可以相互替換,,而用哪個演算法由外部調用者決定。
-
當如果具體策略類超過4個,需要使用混合模式減少類膨脹和對外暴露的問題,通過其他模式修正:工廠方法模式,代理模式,享元模式
策略模式的優缺點
一個設計模式的引入必存在它合理的地方和不足,最後我們再說說下策略模式的優缺點。
優點
- 使用策略模式,可以在不修改原有系統的基礎上更換演算法或行為,可以靈活地增加新的演算法或行為,提供了系統的擴展性
- 策略模式提供了對一類演算法進行管理維護。
- 使用策略模式可以避免使用多重條件判斷,由外部模組決定所要執行的策略類。
缺點
- 客戶端必須知道所有的策略類,並自行決定使用哪一個策略類。
- 會產生很多策略類,使得類的項目增多。
結語
到這裡,本文對策略模式的學習就此結束,當然關於策略模式的內容遠不止這些,配合其他模式還有用法,感興趣了的同學可以參考文末提供的資料鏈接進一步深入學習。也歡迎掃碼關注微信公眾號:「聞人的技術部落格」,定期分享Java技術乾貨,共同進步。
推薦閱讀
參考
- https://en.wikipedia.org/wiki/Strategy_pattern
- https://design-patterns.readthedocs.io/zh_CN/latest/behavioral_patterns/strategy.html#id3
- 設計模式之禪#第18章 策略模式
- Java 8 實戰#8.2 使用 Lambda 重構面向對象的設計模式
-
Java Design Patterns #15. Strategy (Policy) Pattern
本文由部落格一文多發平台 OpenWrite 發布!