算法的封裝與切換——策略模式

轉:http://blog.csdn.net/lovelion/article/details/7818983

 

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

      在軟件開發中,我們也常常會遇到類似的情況,實現某一個功能有多條途徑,每一條途徑對應一種算法,此時我們可以使用一種設計模式來實現靈活地選擇解決途徑,也能夠方便地增加新的解決途徑。本章我們將介紹一種爲了適應算法靈活性而產生的設計模式——策略模式

 

1 電影票打折方案

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

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

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

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

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

      爲了實現上述電影票打折功能,Sunny軟件公司開發人員設計了一個電影票類MovieTicket,其核心代碼片段如下所示:

//電影票類
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; //如果不滿足任何打折要求,則返回原始票價
		}
	}
}

 編寫如下客戶端測試代碼:

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的作用,其結構如圖24-1所示:

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

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

      ● Strategy(抽象策略類):它爲所支持的算法聲明瞭抽象方法,是所有策略類的父類,它可以是抽象類或具體類,也可以是接口。環境類通過抽象策略類中聲明的方法在運行時調用具體策略類中實現的算法。

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

 

思考

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

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

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

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

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

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

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

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軟件公司開發人員使用策略模式對電影院打折方案進行重構,重構後基本結構如圖24-2所示:

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

//電影票類:環境類
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);
	}
}

//折扣類:抽象策略類
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類的代碼如下所示:

import javax.xml.parsers.*;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
import java.io.*;
class XMLUtil {
//該方法用於從XML配置文件中提取具體類類名,並返回一個實例對象
	public static Object getBean() {
		try {
			//創建文檔對象
			DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = dFactory.newDocumentBuilder();
			Document doc;							
			doc = builder.parse(new File("config.xml")); 
		
			//獲取包含類名的文本節點
			NodeList nl = doc.getElementsByTagName("className");
            Node classNode=nl.item(0).getFirstChild();
            String cName=classNode.getNodeValue();
            
            //通過類名生成實例對象並將其返回
            Class c=Class.forName(cName);
	  	    Object obj=c.newInstance();
            return obj;
        }   
        catch(Exception e) {
           	e.printStackTrace();
           	return null;
       	}
    }
}

在配置文件config.xml中存儲了具體策略類的類名,代碼如下所示:

<?xml version="1.0"?>
<config>
    <className>StudentDiscount</className>
</config>

 編寫如下客戶端測試代碼:

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 = (Discount)XMLUtil.getBean(); //讀取配置文件並反射生成具體折扣對象
		mt.setDiscount(discount); //注入折扣對象
		
		currentPrice = mt.getPrice();
		System.out.println("折後價爲:" + currentPrice);
	}
}

編譯並運行程序,輸出結果如下:

原始價爲:60.0

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

學生票:

折後價爲:48.0

      如果需要更換具體策略類,無須修改源代碼,只需修改配置文件,例如將學生票改爲兒童票,只需將存儲在配置文件中的具體策略類StudentDiscount改爲ChildrenDiscount,如下代碼所示:

<?xml version="1.0"?>
<config>
    <className>ChildrenDiscount</className>
</config>

重新運行客戶端程序,輸出結果如下:

原始價爲:60.0

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

兒童票:

折後價爲:50.0

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

 

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

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

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

      在Java SE開發中,用戶需要對容器對象Container中的成員對象如按鈕、文本框等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所示:

      在圖24-4中,OrderSynchronous以一種同步的方式處理訂單,而OrderAsynchronous先將訂單存放在一個隊列中,然後再對隊列裏的訂單進行處理,以一種異步方式對訂單進行處理。BLLOrder類通過反射機制從配置文件中讀取策略配置的信息,以決定到底是使用哪種訂單處理方式。配置文件web.config中代碼片段如下所示:

……
<add key="OrderStrategyClass" value="PetShop.BLL.OrderSynchronous"/>
……
<span style="color:#000000;WIDOWS: 2; TEXT-TRANSFORM: none; BACKGROUND-COLOR: rgb(255,255,255); TEXT-INDENT: 0px; LETTER-SPACING: normal; DISPLAY: inline !important; FONT: 18px/26px Arial; WHITE-SPACE: normal; ORPHANS: 2; FLOAT: none; WORD-SPACING: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px">用戶只需要修改配置文件即可更改訂單處理方式,提高了系統的靈活性。</span>

 

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)

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

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