目錄
本文的結構如下:
- 引言
- 什么是策略模式
- 模式的結構
- 典型代碼
- 代碼示例
- 策略模式和模板方法模式的區別
- 優點和缺點
- 適用環境
- 模式應用
一、引言
寫這篇文章的時間是17年11月18號上午9點半,NBA正打得火熱,騎士VS76人,詹韋對陣大帝和西帝,內線孱弱的騎士被大帝蹂躪,欲生欲死;再看火箭VS籃網,第一節哈登7中7,三分5中5,狂砍20分;又有步行者VS魔術,奧拉迪波10中10......可謂精彩。
我們的話題由此展開。
籃球而言,對陣雙方4節比賽結束后,哪隊分數高,哪隊獲勝,打平加時繼續較量。所以,說到最后,籃球是一個得分的比賽,而得分的方式又有許多種,庫日天的三分,老詹的上籃,德羅贊的中投,哈登的罰球(我不是黑,不要噴我,只是選擇比較有代表性的得分方式)。具體的得分方式根據防守而定,籃下空無一人果斷暴扣,三分線外沒人防守,三分扔起,當然老漢日常碧浪就不說了......
在軟件開發中,也有很多類似的場景,實現某一個功能有多條途徑,每一條途徑對應一種算法,此時可以使用一種設計模式來實現靈活地選擇解決途徑,也能夠方便地增加新的解決途徑。這就是策略模式。
二、什么是策略模式
在策略模式中,定義了一些獨立的類來封裝不同的算法,每一個類封裝一種具體的算法,在這里,每一個封裝算法的類都可以稱之為一種策略(Strategy),為了保證這些策略在使用時具有一致性,一般會提供一個抽象的策略類來做規則的定義,而每種算法則對應于一個具體策略類。
策略模式的主要目的是將算法的定義與使用分開,也就是將算法的行為和環境分開,將算法的定義放在專門的策略類中,每一個策略類封裝了一種實現算法,使用算法的環境類針對抽象策略類進行編程,符合“依賴倒轉原則”。在出現新的算法時,只需要增加一個新的實現了抽象策略類的具體策略類即可。
策略模式定義如下:
策略模式(Strategy Pattern):定義一系列算法類,將每一個算法封裝起來,并讓它們可以相互替換,策略模式讓算法獨立于使用它的客戶而變化,也稱為政策模式(Policy)。策略模式是一種對象行為型模式。
三、模式的結構
策略模式的UML類圖如下:
在策略模式結構圖中包含如下幾個角色:
- Context(環境類):環境類是使用算法的角色,它在解決某個問題(即實現某個方法)時可以采用多種策略。在環境類中維持一個對抽象策略類的引用實例,用于定義所采用的策略。
- Strategy(抽象策略類):它為所支持的算法聲明了抽象方法,是所有策略類的父類,它可以是抽象類或具體類,也可以是接口。環境類通過抽象策略類中聲明的方法在運行時調用具體策略類中實現的算法。
- ConcreteStrategy(具體策略類):它實現了在抽象策略類中聲明的算法,在運行時,具體策略類將覆蓋在環境類中定義的抽象策略類對象,使用一種具體的算法實現某個業務處理。
四、典型代碼
策略模式是一個比較容易理解和使用的設計模式,策略模式是對算法的封裝,它把算法的責任和算法本身分割開,委派給不同的對象管理。策略模式通常把一個系列的算法封裝到一系列具體策略類里面,作為抽象策略類的子類。環境類是需要使用算法的類。在一個系統中可以存在多個環境類,它們可能需要重用一些相同的算法。
在使用策略模式時,首先應該創建一個抽象策略類,其典型代碼如下所示:
public abstract class AbstractStrategy {
public abstract void algorithm();
}
封裝每一種具體算法的類作為該抽象策略類的子類,期典型代碼如下:
public class ConcreteStrategyA extends AbstractStrategy {
public void algorithm() {
//todo
}
}
public class ConcreteStrategyB extends AbstractStrategy {
public void algorithm() {
//todo
}
}
在Context類與抽象策略類之間建立一個關聯關系,其典型代碼如下:
public class Context {
private AbstractStrategy strategy;
public Context(AbstractStrategy strategy){
this.strategy = strategy;
}
public void algorithm(){
strategy.algorithm();
}
}
客戶端代碼如下:
public class Client {
public static void main(String[] args) {
AbstractStrategy strategy = new ConcreteStrategyA();
Context context = new Context(strategy);
context.algorithm();
}
}
也可以將具體策略類類名存儲在配置文件中,通過反射來動態創建具體策略對象,從而使得用戶可以靈活地更換具體策略類,增加新的具體策略類也很方便。策略模式提供了一種可插入式(Pluggable)算法的實現方案。
五、代碼示例
這里以一個球員上場打球為例說明。
5.1、不用策略模式
設計一個球員類:
public class Player {
private String defensive;//對手的防守情況
public Player(){
}
public void setDefensive(String defensive){
this.defensive = defensive;
}
public void score(){
if ("undefended".equalsIgnoreCase(defensive)){
System.out.println("籃下無人防守,他起飛,大風車戰斧扣籃,但是扣飛了,得0分,what a pity!");
}else if("foul".equalsIgnoreCase(defensive)){
System.out.println("他身上掛著三個人,強起上籃命中,哨響了,and one,他走上罰球線,加罰不中,得兩分");
}else if("1s".equalsIgnoreCase(defensive)){
System.out.println("比賽剩下最后1s,還落后2分,現在比賽回來,他接到了球,沒時間了,后撤步直接射三分,有沒有?命中了,絕殺,絕殺。");
}
}
}
客戶端測試:
public class Client {
public static void main(String[] args) {
Player player = new Player();
String defensive;//對手防守情況
player.setDefensive("undefended");
player.score();
System.out.println("------------------------");
player.setDefensive("foul");
player.score();
System.out.println("------------------------");
player.setDefensive("1s");
player.score();
System.out.println("------------------------");
}
}
這段代碼實現了根據不同防守得分的方式,防守方式不同,直接修改客戶端的參數,而不需修改源碼,但還是有一些問題:
- 它包含各種得分的方式,代碼很多且復雜,if...else...多,不利于維護和擴展。
- 增加新的得分方式必須修改源碼,違反了“開閉原則”,系統的靈活性和可擴展性較差。
- 復用性差,如果另一個球員想要復用這些得分方式,只能將源碼粘貼復制,無法單獨重用其中的某個或某些算法(重用較為麻煩)。
5.1、使用策略模式
重構后Basket充當抽象策略類,FreeThrows、SlamDunk和ThreePointer充當具體策略類,Player充當環境類。
代碼如下:
策略類:
public abstract class Basket {
public abstract void score();
}
public class SlamDunk extends Basket{
public void score() {
System.out.println("籃下無人防守,他起飛,大風車戰斧扣籃,但是扣飛了,得0分,what a pity!");
}
}
public class FreeThrows extends Basket{
public void score() {
System.out.println("他身上掛著三個人,強起上籃命中,哨響了,and one,他走上罰球線,加罰不中,得兩分");
}
}
public class ThreePointer extends Basket {
public void score() {
System.out.println("比賽剩下最后1s,還落后2分,現在比賽回來,他接到了球,沒時間了,后撤步直接射三分,有沒有?命中了,絕殺,絕殺。");
}
}
環境類:
public class Player {
private Basket basket;
public Player(Basket basket){
this.basket = basket;
}
public void score(){
basket.score();
}
}
客戶端:
public class Client {
public static void main(String[] args) {
Basket basket = new ThreePointer();
Player player = new Player(basket);
player.score();
}
}
重構后的代碼,如果需要增加新的得分方式,原有代碼均無須修改,只要增加一個新的得分類作為抽象得分類的子類,實現在抽象得分類中聲明的得分方法,然后可以通過配置文件的方式更換具體子類,完全符合“開閉原則”。
六、策略模式和模板方法模式的區別
Template Method模式是將相同的算法放在一個類中,將算法變化的部分放在子類中實現,策略模式是將不同的算法用不同的策略類表示,似乎沒有太大的區別。
認真想想還是有區別的。
策略模式的策略類中的方法一般是public的,封裝的算法是任意給用戶使用,而模板方法模式中的虛方法更多的是有限制的,一般不希望被外部調用,而是在模板方法中在固定的順序位置被調用。
七、優點和缺點
7.1、優點
策略模式的主要優點如下:
- 策略模式提供了對“開閉原則”的完美支持,用戶可以在不修改原有系統的基礎上選擇算法或行為,也可以靈活地增加新的算法或行為。
- 策略模式提供了管理相關的算法族的辦法。策略類的等級結構定義了一個算法或行為族,恰當使用繼承可以把公共的代碼移到抽象策略類中,從而避免重復的代碼。
- 策略模式提供了一種可以替換繼承關系的辦法。如果不使用策略模式,那么使用算法的環境類就可能會有一些子類,每一個子類提供一種不同的算法。但是,這樣一來算法的使用就和算法本身混在一起,不符合“單一職責原則”,決定使用哪一種算法的邏輯和該算法本身混合在一起,從而不可能再獨立演化;而且使用繼承無法實現算法或行為在程序運行時的動態切換。
- 使用策略模式可以避免多重條件選擇語句。多重條件選擇語句不易維護,它把采取哪一種算法或行為的邏輯與算法或行為本身的實現邏輯混合在一起,將它們全部硬編碼(Hard Coding)在一個龐大的多重條件選擇語句中,比直接繼承環境類的辦法還要原始和落后。
- 策略模式提供了一種算法的復用機制,由于將算法單獨提取出來封裝在策略類中,因此不同的環境類可以方便地復用這些策略類。
7.2、缺點
策略模式的主要缺點如下:
- 客戶端必須知道所有的策略類,并自行決定使用哪一個策略類。這就意味著客戶端必須理解這些算法的區別,以便適時選擇恰當的算法。換言之,策略模式只適用于客戶端知道所有的算法或行為的情況。
- 策略模式將造成系統產生很多具體策略類,任何細小的變化都將導致系統要增加一個新的具體策略類。
- 無法同時在客戶端使用多個策略類,也就是說,在使用策略模式時,客戶端每次只能使用一個策略類,不支持使用一個策略類完成部分功能后再使用另一個策略類來完成剩余功能的情況。
八、適用環境
在以下情況下可以考慮使用策略模式:
- 如果在一個系統里面有許多類,它們之間的區別僅在于它們的行為,那么使用策略模式可以動態地讓一個對象在許多行為中選擇一種行為。
- 一個系統需要動態地在幾種算法中選擇一種,那么可以將這些算法封裝到一個個的具體算法類中,而這些具體算法類都是一個抽象算法類的子類。
- 一個對象有很多的行為,如果不用恰當的模式,這些行為就只好使用多重條件選擇語句來實現。此時,使用策略模式,把這些行為轉移到相應的具體策略類里面,就可以避免使用難以維護的多重條件選擇語句。
- 不希望客戶端知道復雜的、與算法相關的數據結構,在具體策略類中封裝算法與相關的數據結構,可以提高算法的保密性與安全性。
九、模式應用
Java SE的容器布局管理就是策略模式的一個經典應用實例,其基本結構如下圖: