Delphi模式程式設計之策略模式(續)
劉藝
1.3 策略模式在飯店管理系統的應用
在飯店管理系統中,通常客房的價格不是一成不變的。對於住宿的淡季和旺季、老客戶和新客戶、散客和團隊,都應該有不同的銷售策略。顯然,銷售策略決定了報價。但是基於銷售策略的報價系統又不能綁定於某一特定的客戶端,因為只有把基於銷售策略的報價系統獨立出來,才能保證其重用性和可維護性。例如:一種報價體系一方面滿足了優惠房價查詢、客房結算等多個客戶端的使用,另一方面又滿足了不斷調整的新銷售策略的需求,這才算真正做到了重用性和可維護性。對於以上的設計要求,選用策略模式是最好不過了。策略模式能夠讓演算法變化獨立於使用它的客戶端。範例程式是一個基於策略模式的優惠房價查詢模組,它包括一個基於銷售策略的報價系統和一個優惠房價查詢介面。當然,優惠房價查詢介面只是此報價系統的客戶端之一,報價系統亦可被其他客戶端使用。優惠房價查詢模組的設計如圖1‑6所示。它包括了:· 銷售策略類TSaleStrategy,它是具體銷售策略類別的抽象基底類別。 · 3個具體銷售策略類別:TVipStrategy (VIP卡銷售策略)、TTeamStrategy (團隊銷售策略)、TSeasonStrategy(季節銷售策略)。 · 報價類別TPRiceContext,它是該策略模式中的上下文,持有一個到TStrategy的引用。 · 客戶端類TClient,它是一個窗體類,即房價查詢的介面。圖1‑6 基於策略模式的優惠房價查詢模組範例程式1‑1是HotelSaleStrategy單元的原始碼,該單元包含了基於銷售策略的報價系統的業務邏輯,以策略模式實現。 TSaleStrategy作為銷售策略的抽象基底類,其目的是提供一個通用的介面。虛抽象函數SalePrice就是這樣一個介面。由於3個具體銷售策略分別是根據季節、VIP卡、團隊人數來制定銷售策略的,所以基類介面SalePrice的參數設計必須滿足3個衍生類別的不同需求。 TSaleStrategy的SalePrice函數宣告如下:function SalePrice(price:Currency;value:integer):Currency; virtual; abstract;它的第一個參數表示傳入的固定房價,第二個參數表示傳入的優惠條件,該條件因不同的衍生類別而異。在季節銷售策略TSeasonStrategy中,此參數表示為入住月份;在VIP卡銷售策略TVIPStrategy中,此參數表示為VIP卡的種類;在團隊銷售策略TTeamStrategy中,此參數表示為團隊人數。我們發現,這些參數都可以用整數型,所以在基底類別中,巧妙地用一個value參數解決了衍生類別的不同參數需求。這樣一來,可以直接讓TPriceContext將資料放在參數中傳遞給不同的銷售策略類別操作,避免了參數冗餘。 {TPriceContext }function TPriceContext.GetPrice(price:Currency;value:integer):Currency;begin result:=Strategy.SalePrice(price,value);end;TPriceContext在該策略模式中起著上下文作用,它負責引用銷售策略物件的不同實例,呼叫SalePrice接口,動態配置具體的折扣演算法,並傳回實際銷售價格。由於有了TPriceContext的中介,客戶端無需知道特定銷售策略是如何實現的;同樣,當銷售策略進行更新調整時,對客戶端程式亦無影響。範例程式1‑1 HotelSaleStrategy單元的原始碼unit HotelSaleStrategy;interfaceuses SysUtils, Windows, Messages, Classes, Graphics, Controls, Forms, Dialogs;type TSaleStrategy = class (TessaObject) public function SalePrice(price:Currency); Currency; virtual; abstract; end; TSeasonStrategy = class (TSaleStrategy) public function SalePrice(price:Currency;value:integer):Currency; override; end; TVIPStrategy = class (TSaleStrategy) public function SalePrice(price:Currrideency; TTeamStrategy = class (TSaleStrategy) public function SalePrice(price:Currency;value:integer):Currency; override; end; TPriceContext = class (TObject) private FStrategy: TSaleStrategy; procedure SetStrategy(Value: TSaleegerStrategy); :Currency; property Strategy: TSaleStrategy read FStrategy write SetStrategy; end;implementation{TSeasonStrategy }function TSeasonStrategy.SalePrice(price:Currency;value:integer):Currency;begin //季節銷售策略{ 2、3、1198.596月優惠,月折優惠。 8、9月9.5折優惠。 } case value of 2,3,11:result:=price*0.85; 4,6:result:=price*0.9; 8,9:result:=price*0.95; else result:=price; end;end;{ TVIPStrategy }function TVIPStrategy.SalePrice(price:Currency;value:integer):Currency;begin //VIP卡銷售策略{ 0:VIP銀卡9折優惠1:VIP金卡8折優惠2:VIP鑽石卡7 折優惠} case value of 0:result:=price*0.9; 1:result:=price *0.8; 2:result:=price*0.7; end;end;{TTeamStrategy }function TTeamStrategy.SalePrice(price:Currency;value:integer):Currency;begin //團隊銷售策略{ 3-5人團隊9折優惠;6-10人團隊8 折優惠;11-20人團隊7 折優惠;20人以上團隊6折優惠。 } result:=price; if (value<6) and (value>2) then result:=price*0.9; if (value<11) and (value>5) then result:=price*0.8; if (value< 21) and (value>10) then result:=price*0.7; if (value>20) then result:=price*0.6;end;{TPriceContext }function TPriceContext.GetPrice(price:Currency;value:integer):Currency;begin result:=Strategy.SalePrice(price,value);end;procedure TPrice:=Strategy.SalePrice(price,value);end;procedure TPrice/ TSaleStrategy);begin FStrategy:=Value;end;end.優惠房價查詢模組的客戶端程式如範例程式1‑2所示。此程式提供一個使用者選擇介面,使得查詢者可以選擇性地選擇一種優惠方案。一旦選定優惠條件和公開房價,點擊「查詢優惠房價」按鈕,便可獲得折扣後的優惠價。實際運作效果如圖1‑7所示。範例程式1‑2 ClientForm單元的原始程式碼unit ClientForm;interfaceuses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ExtCtrls,HotelSaleStrategy, ComCtrls,DateUtilsForm, ExtCtrls,HotelSaleStrategy, ComCtrls,DateUtilstype TClient” : TRadioGroup; btnCheck: TButton; btnExit: TButton; dtpDate: TDateTimePicker; cmbVIP: TComboBox; Label1: TLabel; Label2: TLabel; cmbPrice: TComboBox; edtPrice: TEdit; Label3: TLabelvel; edtabel: procedure FormCreate(Sender: TObject); procedure btnCheckClick(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure btnExitClick(Sender: TObject); procedure RadioGroup1Click(Sender: Tate); FTeamStrategy:TSaleStrategy; FPriceSys:TPriceContext; public { Public declarations } end;var Client: TClient;implementation{$R *.dfm}procedure TClient.FormCreate(Sender: TObject); .Create; FTeamStrategy:=TTeamStrategy.Create; FPriceSys:=TPriceContext.Create;end;procedure TClient.btnCheckClick(Sender: TObject);var i:integer; price:Currency;begin casegyGroup1.ItemIndexea'5:begin, FP; ; i:=MonthOf(dtpDate.DateTime); end; 1:begin FPriceSys.Strategy:=FVIPStrategy ; i:=cmbVIP.ItemIndex; end; 2:begin FPriceSys.Strategy:=FTeamStrategy ; i:=Strategy ; ; end; end; case cmbPrice.ItemIndex of 0:price:=300 ; //甲類標準間300元1:price:=500 ; //乙類標準間500元2:price:=800 ; //貴賓間800元3:price:=1000; //商務套房1000元4:price:=2000; // 豪華套房2000元end; edtPrice.Text:=CurrToStr(FPriceSys.GetPrice(price,i));end;procedure TClient.FormDestroy(Sender: TObject);begin FPriceSys.Free; FSeasonStrategy.Freeend; FVIPStrategy.Free; FTeamStratewient; .btnExitClick(Sender: TObject);begin close;end;procedure TClient.RadioGroup1Click(Sender: TObject);begin dtpDate.Enabled:=false; edtCount.Enabled:=false; cmbVIP.Enabled:=false; case RadioGroup1.ItemIndex of 0:00); 1:cmbVIP.Enabled:=true; 2:edtCount.Enabled:=true; end;end;end.圖1‑7優惠房價查詢模組的實際運作介面
1.4 實踐小結
透過前面範例的示範和剖析,我們進一步討論策略模式如下:· 策略模式提供了管理演算法集的方法。策略類別的層次結構為TContext定義了一系列的可供重複使用的演算法或行為。 TStrategy基類析取出這些演算法中的公共功能,衍生類別透過繼承豐富了演算法的差異和種類,又避免了重複的程式碼。 · 如果不將演算法和使用演算法的上下文分開,直接產生一個包含演算法的TContext類的派生類,給它以不同的行為,這將會把行為寫死到TContext中,而將演算法的實作與TContext的實作混合起來,從而使TContext難以理解、難以維護和難以擴展。最後得到一大堆相關的類別, 它們之間的唯一差別是它們所使用的演算法。顯然,類別的繼承關係是強關聯,繼承關係無法動態地改變演算法;而對象的合成關係是弱關聯,透過組合策略類別對象,使得演算法可以獨立於使用演算法的環境(TContext)而獨立演化。 · 使用策略模式可以對大量使用條件分支語句的程式碼進行重構。當不同的行為堆砌在一個類別中時,很難避免使用條件語句來選擇適當的行為。將行為封裝在一個獨立的策略類別中消除了這些條件語句。 · 過多的演算法可能會導致策略物件的數量很大。為了減少系統開銷,通常可以把依賴演算法環境的狀態保存在客戶端,而將TStrategy實作為可供各客戶端共享的無狀態的物件。任何外部的狀態都由TContext維護。 TContext在每一次對TStrategy物件的請求中都將這個狀態傳遞過去。例如範例程式中,我將TSeasonStrategy的外部狀態入住月份、TVIPStrategy的外部狀態VIP卡的種類、TTeamStrategy的外部狀態團隊人數都保存在客戶端,並透過TPriceContext將這些狀態傳遞給銷售策略類別。這樣做的好處是銷售策略類變成無狀態的了,它們同時可以被客房結算模組等其他模組共享。 · 無論各個具體策略實現的演算法是簡單還是複雜, 它們都共享TStrategy定義的介面。因此很可能某些具體策略不會都用到所有透過這個介面傳遞給它們的資訊。如果我在範例程式中把TSaleStrategy的介面設計成這樣:
SalePrice(price:Currency;Month:integer;VIP:integer;
Count:integer):Currency;
其中的某些參數永遠不會被某些特定銷售策略類用。這就意味著有時TContext會建立和初始化一些永遠不會用到的參數。如果有這樣問題,又無法使用範例程式中的技巧,那麼只能在TStrategy和TContext之間採取緊密耦合的方法。
更多相關文章和範例程式原始碼可以到作者網站下載:http://www.liu-yi.net