1. 程式人生 > >JAVA設計模式(16):行為型-策略模式(Strategy)

JAVA設計模式(16):行為型-策略模式(Strategy)

俗話說:條條大路通羅馬。在很多情況下,實現某個目標的途徑不止一條,例如我們在外出旅遊時可以選擇多種不同的出行方式,如騎自行車、坐汽車、坐火車或者坐飛機,可根據實際情況(目的地、旅遊預算、旅遊時間等)來選擇一種最適合的出行方式。在制訂旅行計劃時,如果目的地較遠、時間不多,但不差錢,可以選擇坐飛機去旅遊;如果目的地雖遠、但假期長、且需控制旅遊成本時可以選擇坐火車或汽車;如果從健康和環保的角度考慮,而且有足夠的毅力,自行車遊或者徒步旅遊也是個不錯的選擇,大笑

      在軟體開發中,我們也常常會遇到類似的情況,實現某一個功能有多條途徑,每一條途徑對應一種演算法,此時我們可以使用一種設計模式來實現靈活地選擇解決途徑,也能夠方便地增加新的解決途徑

。本章我們將介紹一種為了適應演算法靈活性而產生的設計模式——策略模式

1 電影票打折方案

      Sunny軟體公司為某電影院開發了一套影院售票系統,在該系統中需要為不同型別的使用者提供不同的電影票打折方式,具體打折方案如下:

      (1) 學生憑學生證可享受票價8折優惠;

      (2) 年齡在10週歲及以下的兒童可享受每張票減免10元的優惠(原始票價需大於等於20元);

      (3) 影院VIP使用者除享受票價半價優惠外還可進行積分,積分累計到一定額度可換取電影院贈送的獎品。

      該系統在將來可能還要根據需要引入新的打折方式。

      為了實現上述電影票打折功能,Sunny

軟體公司開發人員設計了一個電影票類MovieTicket,其核心程式碼片段如下所示:

//電影票類  
public class MovieTicket {  
    private double price; //電影票價格  
    private String type; //電影票型別  
      
    public void setPrice(double price) {  
        this.price = price;  
    }  
      
    public void setType(String type) {  
        this.type = type;  
    }  
      
    public double getPrice() {  
        return this.calculate();  
    }  
      
    //計算打折之後的票價  
    public double calculate() {  
        //學生票折後票價計算  
        if(this.type.equalsIgnoreCase("student")) {  
            System.out.println("學生票:");  
            return this.price * 0.8;  
        }  
        //兒童票折後票價計算  
        else if(this.type.equalsIgnoreCase("children") && this.price >= 20 ) {  
            System.out.println("兒童票:");  
            return this.price - 10;  
        }  
        //VIP票折後票價計算  
        else if(this.type.equalsIgnoreCase("vip")) {  
            System.out.println("VIP票:");  
            System.out.println("增加積分!");  
            return this.price * 0.5;  
        }  
        else {  
            return this.price; //如果不滿足任何打折要求,則返回原始票價  
        }  
    }  
}  

      編寫如下客戶端測試程式碼:

public class Client {  
    public static void main(String args[]) {  
        MovieTicket mt = new MovieTicket();  
        double originalPrice = 60.0; //原始票價  
        double currentPrice; //折後價  
          
        mt.setPrice(originalPrice);  
        System.out.println("原始價為:" + originalPrice);  
        System.out.println("---------------------------------");  
              
        mt.setType("student"); //學生票  
        currentPrice = mt.getPrice();  
        System.out.println("折後價為:" + currentPrice);  
        System.out.println("---------------------------------");  
          
        mt.setType("children"); //兒童票  
        currentPrice = mt.getPrice();  
        System.out.println("折後價為:" + currentPrice);  
    }  
} 

      編譯並執行程式,輸出結果如下所示:

原始價為:60.0

---------------------------------

學生票:

折後價為:48.0

---------------------------------

兒童票:

折後價為:50.0

      通過MovieTicket類實現了電影票的折後價計算,該方案解決了電影票打折問題,每一種打折方式都可以稱為一種打折演算法,更換打折方式只需修改客戶端程式碼中的引數,無須修改已有原始碼,但該方案並不是一個完美的解決方案,它至少存在如下三個問題:

      (1)MovieTicket類的calculate()方法非常龐大,它包含各種打折演算法的實現程式碼,在程式碼中出現了較長的if…else…語句,不利於測試和維護

      (2) 增加新的打折演算法或者對原有打折演算法進行修改時必須修改MovieTicket類的原始碼,違反了“開閉原則”,系統的靈活性和可擴充套件性較差。

      (3) 法的複用性差,如果在另一個系統(如商場銷售管理系統)中需要重用某些打折演算法,只能通過對原始碼進行復制貼上來重用,無法單獨重用其中的某個或某些演算法(重用較為麻煩)。

      如何解決這三個問題?導致產生這些問題的主要原因在於MovieTicket類職責過重,它將各種打折演算法都定義在一個類中,這既不便於演算法的重用,也不便於演算法的擴充套件。因此我們需要對MovieTicket類進行重構,將原本龐大的MovieTicket類的職責進行分解,將演算法的定義和使用分離,這就是策略模式所要解決的問題,下面將進入策略模式的學習。

2 策略模式概述

      在策略模式中,我們可以定義一些獨立的類來封裝不同的演算法,每一個類封裝一種具體的演算法,在這裡,每一個封裝演算法的類我們都可以稱之為一種策略(Strategy),為了保證這些策略在使用時具有一致性,一般會提供一個抽象的策略類來做規則的定義,而每種演算法則對應於一個具體策略類。

      策略模式的主要目的是將演算法的定義與使用分開,也就是將演算法的行為和環境分開,將演算法的定義放在專門的策略類中,每一個策略類封裝了一種實現演算法,使用演算法的環境類針對抽象策略類進行程式設計,符合“依賴倒轉原則”。在出現新的演算法時,只需要增加一個新的實現了抽象策略類的具體策略類即可。策略模式定義如下:

策略模式(Strategy Pattern):定義一系列演算法類,將每一個演算法封裝起來,並讓它們可以相互替換,策略模式讓演算法獨立於使用它的客戶而變化,也稱為政策模式(Policy)。策略模式是一種物件行為型模式。

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

      在策略模式結構圖中包含如下幾個角色:

      ● Context(環境類):環境類是使用演算法的角色,它在解決某個問題(即實現某個方法)時可以採用多種策略。在環境類中維持一個對抽象策略類的引用例項,用於定義所採用的策略。

      ● Strategy(抽象策略類):它為所支援的演算法聲明瞭抽象方法,是所有策略類的父類,它可以是抽象類或具體類,也可以是介面。環境類通過抽象策略類中宣告的方法在執行時呼叫具體策略類中實現的演算法。

      ● ConcreteStrategy(具體策略類):它實現了在抽象策略類中宣告的演算法,在執行時,具體策略類將覆蓋在環境類中定義的抽象策略類物件,使用一種具體的演算法實現某個業務處理。

思考

一個環境類Context能否對應多個不同的策略等級結構?如何設計?

      策略模式是一個比較容易理解和使用的設計模式,策略模式是對演算法的封裝,它把演算法的責任和演算法本身分割開,委派給不同的物件管理。策略模式通常把一個系列的演算法封裝到一系列具體策略類裡面,作為抽象策略類的子類。在策略模式中,對環境類和抽象策略類的理解非常重要,環境類是需要使用演算法的類。在一個系統中可以存在多個環境類,它們可能需要重用一些相同的演算法。

      在使用策略模式時,我們需要將演算法從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)演算法的實現方案

3 完整解決方案

      為了實現打折演算法的複用,並能夠靈活地向系統中增加新的打折方式,Sunny軟體公司開發人員使用策略模式對電影院打折方案進行重構,重構後基本結構如圖2所示:

      在圖2中,MovieTicket充當環境類角色,Discount充當抽象策略角色,StudentDiscount、 ChildrenDiscount VIPDiscount充當具體策略角色。完整程式碼如下所示:

//電影票類:環境類  
public class MovieTicket {  
    private double price;  
    private Discount discount; //維持一個對抽象折扣類的引用  
  
    public void setPrice(double price) {  
        this.price = price;  
    }  
  
    //注入一個折扣類物件  
    public void setDiscount(Discount discount) {  
        this.discount = discount;  
    }  
  
    public double getPrice() {  
        //呼叫折扣類的折扣價計算方法  
        return discount.calculate(this.price);  
    }  
}  
  
//折扣類:抽象策略類  
public interface Discount {  
    public double calculate(double price);  
}  
  
//學生票折扣類:具體策略類  
class StudentDiscount implements Discount {  
    public double calculate(double price) {  
        System.out.println("學生票:");  
        return price * 0.8;  
    }  
}   
  
//兒童票折扣類:具體策略類  
class ChildrenDiscount implements Discount {  
    public double calculate(double price) {  
        System.out.println("兒童票:");  
        return price - 10;  
    }  
}   
  
//VIP會員票折扣類:具體策略類  
class VIPDiscount implements Discount {  
    public double calculate(double price) {  
        System.out.println("VIP票:");  
        System.out.println("增加積分!");  
        return price * 0.5;  
    }  
}  


      為了提高系統的靈活性和可擴充套件性,我們將具體策略類的類名儲存在配置檔案中,並通過工具類XMLUtil來讀取配置檔案並反射生成物件,XMLUtil類的程式碼如下所示:

public class XMLUtil {  
  //該方法用於從XML配置檔案中提取具體類類名,並返回一個例項物件    
    public static Object getBean() throws Exception {  
        SAXReader reader = new SAXReader();  
        String path = XMLUtil.class.getClassLoader().  
                getResource("com/somnus/designPatterns/strategy/config.xml").getPath();  
        Document document = reader.read(new File(path));  
        String cName = document.selectSingleNode("/config/className").getText();  
        //通過類名生成例項物件並將其返回    
        Class<?> c = Class.forName(cName);    
        Object obj = c.newInstance();    
        return obj;    
    }    
}  

      在配置檔案config.xml中儲存了具體策略類的類名,程式碼如下所示:

<?xml version="1.0" encoding="UTF-8"?>  
<config>    
    <className>com.somnus.designPatterns.strategy.StudentDiscount</className>    
</config>  

      編寫如下客戶端測試程式碼:

public class Client {  
    public static void main(String args[]) {  
        MovieTicket mt = new MovieTicket();  
        double originalPrice = 60.0;  
        double currentPrice;  
          
        mt.setPrice(originalPrice);  
        System.out.println("原始價為:" + originalPrice);  
        System.out.println("---------------------------------");  
              
        Discount discount = (Discount)XMLUtil.getBean(); //讀取配置檔案並反射生成具體折扣物件  
        mt.setDiscount(discount); //注入折扣物件  
          
        currentPrice = mt.getPrice();  
        System.out.println("折後價為:" + currentPrice);  
    }  
}  

      編譯並執行程式,輸出結果如下:

原始價為:60.0

---------------------------------

學生票:

折後價為:48.0

      如果需要更換具體策略類,無須修改原始碼,只需修改配置檔案,例如將學生票改為兒童票,只需將儲存在配置檔案中的具體策略類StudentDiscount改為ChildrenDiscount,如下程式碼所示:

<?xml version="1.0" encoding="UTF-8"?>  
<config>    
    <className>com.somnus.designPatterns.strategy.ChildrenDiscount</className>    
</config>  

      重新執行客戶端程式,輸出結果如下:

原始價為:60.0

---------------------------------

兒童票:

折後價為:50.0

      如果需要增加新的打折方式,原有程式碼均無須修改,只要增加一個新的折扣類作為抽象折扣類的子類,實現在抽象折扣類中宣告的打折方法,然後修改配置檔案,將原有具體折扣類類名改為新增折扣類類名即可,完全符合“開閉原則”。

4 策略模式的兩個典型應用

      策略模式實用性強、擴充套件性好,在軟體開發中得以廣泛使用,是使用頻率較高的設計模式之一。下面將介紹策略模式的兩個典型應用例項,一個來源於,一個來源於微軟公司推出的演示專案PetShop

      (1) Java SE的容器佈局管理就是策略模式的一個經典應用例項,其基本結構示意圖如圖3所示:

【每次看到這個LayoutManager2介面,我都在想當時Sun公司開發人員是怎麼想的!微笑

      在Java SE開發中,使用者需要對容器物件中的成員物件如按鈕、文字框等GUI控制元件進行佈局(Layout),在程式執行期間由客戶端動態決定一個Container物件如何佈局,Java語言在JDK中提供了幾種不同的佈局方式,封裝在不同的類中,如BorderLayoutFlowLayoutGridLayoutGridBagLayoutCardLayout等。在圖24-3中,Container類充當環境角色Context,而LayoutManager作為所有佈局類的公共父類扮演了抽象策略角色,它給出所有具體佈局類所需的介面,而具體策略類是LayoutManager的子類,也就是各種具體的佈局類,它們封裝了不同的佈局方式。

      任何人都可以設計並實現自己的佈局類,只需要將自己設計的佈局類作為LayoutManager的子類就可以,比如傳奇的Borland公司(現在已是傳說,難過曾在JBuilder中提供了一種新的佈局方式——XYLayout,作為對JDK提供的Layout類的補充。對於客戶端而言,只需要使用Container類提供的setLayout()方法就可設定任何具體佈局方式,無須關心該佈局的具體實現。在JDK中,Container類的程式碼片段如下:

public class Container extends Component {  
    ……  
    LayoutManager layoutMgr;  
    ……  
    public void setLayout(LayoutManager mgr) {  
    layoutMgr = mgr;  
    ……  
    }  
    ……  
}  

      從上述程式碼可以看出,Container作為環境類,針對抽象策略類LayoutManager進行程式設計,使用者在使用時,根據“里氏代換原則”,只需要在setLayout()方法中傳入一個具體佈局物件即可,無須關心它的具體實現。

      (2) 除了基於Java語言的應用外,在使用其他面向物件技術開發的軟體中,策略模式也得到了廣泛的應用。

      在微軟公司提供的演示專案PetShop 4.0中就使用策略模式來處理同步訂單和非同步訂單的問題。在PetShop 4.0BLLBusiness Logic Layer,業務邏輯層)子專案中有一個OrderAsynchronous類和一個OrderSynchronous類,它們都繼承自IOrderStrategy介面,如圖24-4所示:

      在圖4中,OrderSynchronous以一種同步的方式處理訂單,而OrderAsynchronous先將訂單存放在一個佇列中,然後再對佇列裡的訂單進行處理,以一種非同步方式對訂單進行處理。BLLOrder類通過反射機制從配置檔案中讀取策略配置的資訊,以決定到底是使用哪種訂單處理方式。配置檔案web.config中程式碼片段如下所示:

……  
<add key="OrderStrategyClass" value="PetShop.BLL.OrderSynchronous"/>  
……  

     使用者只需要修改配置檔案即可更改訂單處理方式,提高了系統的靈活性。

5 策略模式總結

策略模式用於演算法的自由切換和擴充套件,它是應用較為廣泛的設計模式之一。策略模式對應於解決某一問題的一個演算法族,允許使用者從該演算法族中任選一個演算法來解決某一問題,同時可以方便地更換演算法或者增加新的演算法。只要涉及到演算法的封裝、複用和切換都可以考慮使用策略模式。

      1. 主要優點

      策略模式的主要優點如下:

      (1) 策略模式提供了對“開閉原則”的完美支援,使用者可以在不修改原有系統的基礎上選擇演算法或行為,也可以靈活地增加新的演算法或行為

      (2) 策略模式提供了管理相關的演算法族的辦法。策略類的等級結構定義了一個演算法或行為族,恰當使用繼承可以把公共的程式碼移到抽象策略類中,從而避免重複的程式碼。

      (3) 策略模式提供了一種可以替換繼承關係的辦法。如果不使用策略模式,那麼使用演算法的環境類就可能會有一些子類,每一個子類提供一種不同的演算法。但是,這樣一來演算法的使用就和演算法本身混在一起,不符合“單一職責原則”,決定使用哪一種演算法的邏輯和該演算法本身混合在一起,從而不可能再獨立演化;而且使用繼承無法實現演算法或行為在程式執行時的動態切換。

      (4) 使用策略模式可以避免多重條件選擇語句。多重條件選擇語句不易維護,它把採取哪一種演算法或行為的邏輯與演算法或行為本身的實現邏輯混合在一起,將它們全部硬編碼(Hard Coding)在一個龐大的多重條件選擇語句中,比直接繼承環境類的辦法還要原始和落後。

      (5) 策略模式提供了一種演算法的複用機制,由於將演算法單獨提取出來封裝在策略類中,因此不同的環境類可以方便地複用這些策略類。

      2. 主要缺點

      策略模式的主要缺點如下:

      (1) 客戶端必須知道所有的策略類,並自行決定使用哪一個策略類。這就意味著客戶端必須理解這些演算法的區別,以便適時選擇恰當的演算法。換言之,策略模式只適用於客戶端知道所有的演算法或行為的情況。

      (2) 策略模式將造成系統產生很多具體策略類,任何細小的變化都將導致系統要增加一個新的具體策略類。

      (3) 無法同時在客戶端使用多個策略類,也就是說,在使用策略模式時,客戶端每次只能使用一個策略類,不支援使用一個策略類完成部分功能後再使用另一個策略類來完成剩餘功能的情況。

      3. 適用場景

      在以下情況下可以考慮使用策略模式:

      (1) 一個系統需要動態地在幾種演算法中選擇一種,那麼可以將這些演算法封裝到一個個的具體演算法類中,而這些具體演算法類都是一個抽象演算法類的子類。換言之,這些具體演算法類均有統一的介面,根據“里氏代換原則”和麵向物件的多型性,客戶端可以選擇使用任何一個具體演算法類,並只需要維持一個數據型別是抽象演算法類的物件。

      (2) 一個物件有很多的行為,如果不用恰當的模式,這些行為就只好使用多重條件選擇語句來實現。此時,使用策略模式,把這些行為轉移到相應的具體策略類裡面,就可以避免使用難以維護的多重條件選擇語句。

      (3) 不希望客戶端知道複雜的、與演算法相關的資料結構,在具體策略類中封裝演算法與相關的資料結構,可以提高演算法的保密性與安全性。

練習

    Sunny軟體公司欲開發一款飛機模擬系統,該系統主要模擬不同種類飛機的飛行特徵與起飛特徵,需要模擬的飛機種類及其特徵如表24-1所示:

24-1 飛機種類及特徵一覽表

飛機種類

起飛特徵

飛行特徵

直升機(Helicopter)

垂直起飛(VerticalTakeOff)

亞音速飛行(SubSonicFly)

客機(AirPlane)

長距離起飛(LongDistanceTakeOff)

亞音速飛行(SubSonicFly)

殲擊機(Fighter)

長距離起飛(LongDistanceTakeOff)

超音速飛行(SuperSonicFly)

鷂式戰鬥機(Harrier)

垂直起飛(VerticalTakeOff)

超音速飛行(SuperSonicFly)

      為將來能夠模擬更多種類的飛機,試採用策略模式設計該飛機模擬系統。