設計模式(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 ;
}
}
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 ;
}
@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
《道德經》第四章:
道沖,而用之有弗盈也。淵呵!似萬物之宗。锉其兌,解其紛,和其光,同其塵。湛呵!似或存。吾不知其誰之子,象帝之先。
譯文:大“道”空虛開形,但它的作用又是無窮無盡。深遠啊!它好象萬物的祖宗。消磨它的鋒銳,消除它的紛擾,調和它的光輝,混同于塵垢。隐沒不見啊,又好象實際存在。我不知道它是誰的後代,似乎是天帝的祖先。