設計模式(1):行爲型-策略模式(Strategy)

設計模式(Design pattern)是一套被反覆使用、多數人知曉的、經過分類編目的、代碼設計經驗的總結。使用設計模式是爲了可重用代碼、讓代碼更容易被他人理解、保證代碼可靠性。 毫無疑問,設計模式於己於他人於系統都是多贏的;設計模式使代碼編制真正工程化;設計模式是軟件工程的基石脈絡,如同大廈的結構一樣。

設計模式分爲三種類型,共23種。
創建型模式(5):單例模式、抽象工廠模式、建造者模式、工廠模式、原型模式。
結構型模式(7):適配器模式、橋接模式、裝飾模式、組合模式、外觀模式、享元模式、代理模式。
行爲型模式(11)(父子類)策略模式、模版方法模式,(兩個類)觀察者模式、迭代器模式、職責鏈模式、命令模式,(類的狀態)狀態模式、備忘錄模式,(中間類) 訪問者模式、中介者模式、解釋器模式。

一.概述

定義

   Strategy Pattern(策略模式):定義一系列算法類,將每一個算法封裝起來,並讓它們可以相互替換,策略模式讓算法獨立於使用它的客戶而變化,也稱爲政策模式(Policy)。策略模式是一種對象行爲型模式。
  Strategy Pattern:Define a family of algorithms, encapsulate each one, and make them interchangeable.

結構

策略模式結構並不複雜,但我們需要理解其中環境類Context的作用,其結構如圖所示:
這裏寫圖片描述

策略模式涉及到三個角色:

  • 環境(Context)角色:環境類是使用算法的角色,它在解決某個問題(即實現某個方法)時可以採用多種策略。在環境類中維持一個對抽象策略類的引用實例,用於定義所採用的策略。
  • 抽象策略(Strategy)角色:它爲所支持的算法聲明瞭抽象方法,是所有策略類的父類,它可以是抽象類或具體類,也可以是接口。環境類通過抽象策略類中聲明的方法在運行時調用具體策略類中實現的算法。
  • 具體策略(ConcreteStrategy)角色:它實現了在抽象策略類中聲明的算法,在運行時,具體策略類將覆蓋在環境類中定義的抽象策略類對象,使用一種具體的算法實現某個業務處理。
      策略模式是一個比較容易理解和使用的設計模式,策略模式是對算法的封裝,它把算法的責任和算法本身分割開,委派給不同的對象管理。策略模式通常把一個系列的算法封裝到一系列具體策略類裏面,作爲抽象策略類的子類。在策略模式中,對環境類和抽象策略類的理解非常重要,環境類是需要使用算法的類。在一個系統中可以存在多個環境類,它們可能需要重用一些相同的算法。

實現

  在使用策略模式時,我們需要將算法從Context類中提取出來,首先應該創建一個抽象策略類,其典型代碼如下所示:

public abstract class AbstractStrategy {    
    public abstract void algorithm(); //聲明抽象算法    
}    

然後再將封裝每一種具體算法的類作爲該抽象策略類的子類,如下代碼所示:

public class ConcreteStrategyA extends AbstractStrategy {    
    //算法的具體實現    
    public void algorithm() {    
       //算法A    
    }    
}   

其他具體策略類與之類似,對於Context類而言,在它與抽象策略類之間建立一個關聯關係,其典型代碼如下所示:
 

public class Context {    
    private AbstractStrategy strategy; //維持一個對抽象策略類的引用    

    public void setStrategy(AbstractStrategy strategy) {    
        this.strategy= strategy;    
    }    

    //調用策略類中的算法    
    public void algorithm() {    
        strategy.algorithm();    
    }    
}    

在Context類中定義一個AbstractStrategy類型的對象strategy,通過注入的方式在客戶端傳入一個具體策略對象,客戶端代碼片段如下所示:

……    
Context context = new Context();    
AbstractStrategy strategy;    
strategy = new ConcreteStrategyA(); //可在運行時指定類型    
context.setStrategy(strategy);    
context.algorithm();    
……    

在客戶端代碼中只需注入一個具體策略對象,可以將具體策略類類名存儲在配置文件中,通過反射來動態創建具體策略對象,從而使得用戶可以靈活地更換具體策略類,增加新的具體策略類也很方便。策略模式提供了一種可插入式(Pluggable)算法的實現方案。
 

二.示例

下面舉個排序算法的例子:

public interface SortUtil{
    <T extends Comparable<?>> T[] sortList(T[] list);
}
public class BubbleSort implements SortUtil{
    @Override
    public <T extends Comparable<?>> T[] sortList(T[] list) {
        System.out.println("採用了冒泡排序算法!");
        return list;
    }
}
public class HashSort implements SortUtil{
    @Override
    public <T extends Comparable<?>> T[] sortList(T[] list) {
        System.out.println("採用了哈希排序算法!");
        return list;
    }
}
public class Client {
    public static void main(String[] args) {
        String[] a5 = new String[] {"cc","11","Dd","2","5"};
        //選擇並創建需要使用的策略對象
        SortUtil sortUtil = new BubbleSort();//此處高度依賴具體實現類BubbleSort
        sortUtil.sortList(a5);
    }

}

由於比較簡單,這裏省略了環境類。

三.總結

策略模式的重心

 策略模式的重心不是如何實現算法,而是如何組織、調用這些算法,從而讓程序結構更靈活,具有更好的維護性和擴展性。

算法的平等性

策略模式一個很大的特點就是各個策略算法的平等性。對於一系列具體的策略算法,大家的地位是完全一樣的,正因爲這個平等性,才能實現算法之間可以相互替換。所有的策略算法在實現上也是相互獨立的,相互之間是沒有依賴的。
  所以可以這樣描述這一系列策略算法:策略算法是相同行爲的不同實現。

運行時策略的唯一性

運行期間,策略模式在每一個時刻只能使用一個具體的策略實現對象,雖然可以動態地在不同的策略實現中切換,但是同時只能使用一個。

策略模式的優點

(1)策略模式提供了管理相關的算法族的辦法。策略類的等級結構定義了一個算法或行爲族。恰當使用繼承可以把公共的代碼移到父類裏面,從而避免代碼重複。
(2)使用策略模式可以避免使用多重條件(if-else)語句。多重條件語句不易維護,它把採取哪一種算法或採取哪一種行爲的邏輯與算法或行爲的邏輯混合在一起,統統列在一個多重條件語句裏面,比使用繼承的辦法還要原始和落後。

策略模式的缺點

(1)客戶端必須知道所有的策略類,並自行決定使用哪一個策略類。這就意味着客戶端必須理解這些算法的區別,以便適時選擇恰當的算法類。換言之,策略模式只適用於客戶端知道算法或行爲的情況。
(2)由於策略模式把每個具體的策略實現都單獨封裝成爲類,如果備選的策略很多的話,那麼對象的數目就會很可觀。

四.拓展

緊耦合tight coupling是指兩個實體高度依賴彼此以至於改變其中某個的行爲時,需要調整實際的其中一個甚至二者的代碼。鬆耦合 loose coupling則與之相反,兩個實體沒有高度依賴,它們之間甚至不知道彼此的存在,但二者仍然可以互相交互。

從上面總結可以看出策略模式也有不足的,顯然客戶端與具體策略類是緊耦合的,即客戶端需要知道所有策略類並自行決定使用其中的一個(高度依賴)。另外策略算法的替換(或刪除)也會要求客戶端更改相應的代碼,那有沒有什麼方式可以讓客戶端不需要知道具體策略類,就可以使用策略類,甚至可以讓策略的選擇權交給策略端(服務端)來實現呢?有的!那就是Services!

在Java6中引入了一個類叫ServiceLoader,它是一個簡單的service provider服務提供者的裝載工具類。
service(服務)是一組定義清晰的接口。service provider(服務提供者)是service的具體實現。

那就用service來改造下上面的例子,讓客戶端只面對接口編程,使得客戶端與具體實現了完全解耦。

public interface SortUtil{
    SortName getName();//策略參數,按名稱來採用算法
    <T extends Comparable<?>> T[] sortList(T[] list);
    public static SortUtil getSortInstance(SortName name) {
        ServiceLoader<SortUtil> sortUtils = ServiceLoader.load(SortUtil.class);
        for (SortUtil s : sortUtils) {
            if (name == s.getName()) {
                return s;
            }
        }
        return null;
    }
    //算法名稱枚舉
    public static enum SortName{
        BUBBLE,HASH,HEAP,MERGE;
    }
}
public class BubbleSort implements SortUtil{
    @Override
    public <T extends Comparable<?>> T[] sortList(T[] list) {
        System.out.println("採用了冒泡排序算法!");
        return list;
    }
    @Override
    public SortName getName() {
        return SortName.BUBBLE;
    }
}
public class HashSort implements SortUtil{
    @Override
    public <T extends Comparable<?>> T[] sortList(T[] list) {
        System.out.println("採用了哈希排序算法!");
        return list;
    }
    @Override
    public SortName getName() {
        return SortName.HASH;
    }
}
public class Client {
    public static void main(String[] args) {
        String[] a5 = new String[] {"cc","11","Dd","2","5"};
        //根據策略參數獲取算法實例,與實現類是完全解耦的
        SortUtil sortUtil = SortUtil.getSortInstance(SortName.HASH);
        sortUtil.sortList(a5);
    }
}

由於ServiceLoader 實現了Iterable接口,所以方法 ServiceLoader ServiceLoader.load(Class service),返回指定的service服務的所有實現者的集合的迭代器。它是一種懶加載的方式,僅當實例化某個具體實現類後,就把它加到緩存中。再利用java8的API的新特性,可以使用接口的靜態方法來獲取實例。這樣做的好處是客戶端只需通過接口(及參數)就可獲取實現類的實例,使得客戶端與實現類完全解耦。

當然ServiceLoader.load需要通過配置文件來加載所有的實現類的,它通過service的全稱例如strategy.api.SortUtil,然後在類路徑META-INF/services/ 目錄下尋找strategy.api.SortUtil文件,所以需要創建這麼個文件。文件內容的每一行爲一個具體實現類的全名。

這裏寫圖片描述

但是,好像替換或刪除掉某個算法實現類後,客戶端還是需要更改代碼(例如上面的HASH算法類刪掉了,客戶也要改代碼),這種情況也是有可能發生的。比如某個jar包提供了上面多種算法的實現類庫,客戶端使用了該jar包的類A,後來jar包升級後將其中某個很差勁的算法類A給刪除了(這種做法通常不可取),而客戶端也升級了這個jar包,那客戶端由於缺少這個類A,那客戶端原來的程序將會無法編譯成功,而迫使修改代碼。

雖然解耦了客戶端和實現類的關係,但策略選擇權還是在客戶端,客戶需要根據接口中提供的算法名稱來決定使用哪一個算法。那如果把選擇權移到服務端(接口)會怎麼樣呢?看下面代碼:

public interface SortUtil{
    int getIdealMaxInputLength();// 策略參數,根據最大的數目決定是否採用此算法
    <T extends Comparable<?>> T[] sortList(T[] list);
    public static SortUtil getSortInstance(int listSize) {
        ServiceLoader<SortUtil> sortUtils = ServiceLoader.load(SortUtil.class);
        List<SortUtil> list = new ArrayList<>();
        for (SortUtil sortUtil : sortUtils) {
            list.add(sortUtil);
        }
        Collections.sort(list);
        for (SortUtil s : list) {
            if (listSize <= s.getIdealMaxInputLength()) {
                return s;
            }
        }
        return null;
    }
}
public class BubbleSort implements SortUtil{
    @Override
    public <T extends Comparable<?>> T[] sortList(T[] list) {
        System.out.println("採用了冒泡排序算法!");
        return list;
    }
    @Override
    public int getIdealMaxInputLength() {
        return 4;
    }
}
public class HashSort implements SortUtil{
    @Override
    public <T extends Comparable<?>> T[] sortList(T[] list) {
        System.out.println("採用了哈希排序算法!");
        return list;
    }
    @Override
    public int getIdealMaxInputLength() {
        return Integer.MAX_VALUE;
    }
}
public class MergeSort implements SortUtil {
    @Override
    public int getIdealMaxInputLength() {
        return 8;
    }

    @Override
    public <T extends Comparable<?>> T[] sortList(T[] list) {
        // 此處對list排序
        System.out.println("採用了歸併排序算法!");
        return list;
    }
}
public class Client {
    public static void main(String[] args) {
        String[] a5 = new String[] {"cc","11","Dd","2","5"};
        //根據策略參數獲取算法實例,與實現類是完全解耦的
        SortUtil sortUtil = SortUtil.getSortInstance(a5.length);
        sortUtil.sortList(a5);
    }
}

這樣客戶端已經與接口實現類徹徹底底的解耦了,客戶端完全無法感知實現類的存在。無論添加、刪除、替換掉某種算法,客戶端都不需要動代碼。當然不完美之處是損失了ServiceLoader的懶加載特性,因爲需要遍歷所有實例才能決定採用哪個實例,但保留了其緩存的特點。另外也在一定程度上使得各個實現類之間有了某種聯繫(根據參數來選擇某個實現類實例)。有時找不到實例會返回null,需要客戶端作相應處理(例如自己實現個默認的實例。

客戶端要使用其中的某個實現類,在Java9之前是完全可以做到的,只需import該類即可。那似乎還是沒有最終解決客戶會直接調用實現類(不通過接口)的方式。

試想下,如果把這些具體實現類單獨封裝成私有包,讓客戶端無法通過import方式來使用這些實現類,那作爲這些類的維護者而言(例如Java平臺本身)可謂真是一大好事,不用再聲明@deprecated廢棄了,直接刪掉即可,也不會影響所有的客戶端。這一想法在Java9模塊化中得以實現,模塊化使得在包層級上私有化成爲可能。(Java9之前,Java平臺中有些類是專門給平臺內部使用的,但還是無法避免客戶端通過Import方式來使用,造成後續平臺維護升級的困難)

參考電子書下載:設計模式的藝術–軟件開發人員內功修煉之道_劉偉(2013年).pdf

《道德經》第四章:
道衝,而用之有弗盈也。淵呵!似萬物之宗。銼其兌,解其紛,和其光,同其塵。湛呵!似或存。吾不知其誰之子,象帝之先。
譯文:大“道”空虛開形,但它的作用又是無窮無盡。深遠啊!它好象萬物的祖宗。消磨它的鋒銳,消除它的紛擾,調和它的光輝,混同於塵垢。隱沒不見啊,又好象實際存在。我不知道它是誰的後代,似乎是天帝的祖先。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章