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;
interface
uses
SysUtils, Windows, Messages, Classes, Graphics, Controls,
Forms, Dialogs;
type
TSaleStrategy = class (TObject)
public
function SalePrice(price:Currency;value:integer):Currency;
virtual; abstract;
end;
TSeasonStrategy = class (TSaleStrategy)
public
function SalePrice(price:Currency;value:integer):Currency; override;
end;
TVIPStrategy = class (TSaleStrategy)
public
function SalePrice(price:Currency;value:integer):Currency; override;
end;
TTeamStrategy = class (TSaleStrategy)
public
function SalePrice(price:Currency;value:integer):Currency; override;
end;
TPriceContext = class (TObject)
private
FStrategy: TSaleStrategy;
procedure SetStrategy(Value: TSaleStrategy);
public
function GetPrice(price:Currency;value:integer):Currency;
property Strategy: TSaleStrategy read FStrategy write SetStrategy;
end;
implementation
{TSeasonStrategy }
function TSeasonStrategy.SalePrice(price:Currency;value:integer):Currency;
begin
//季節銷售策略
{
2、3、11月8.5折優惠,
4、6月9折優惠。
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 TPriceContext.SetStrategy(Value: TSaleStrategy);
begin
FStrategy:=Value;
end;
end.
優惠房價查詢模塊的客戶端程序如示例程序 1‑2所示。該程序提供一個用戶選擇界面,使得查詢者可以任選一種優惠方案。一旦選定優惠條件和公開房價,點擊“查詢優惠房價”按鈕,便得到打折後的優惠價。實際運行效果如圖 1‑7所示。
示例程序 1‑2 ClIEntForm單元的源代碼
unit ClIEntForm;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ExtCtrls,HotelSaleStrategy, ComCtrls,DateUtils;
type
TClIEnt = class(TForm)
RadioGroup1: TRadioGroup;
btnCheck: TButton;
btnExit: TButton;
dtpDate: TDateTimePicker;
cmbVIP: TComboBox;
Label1: TLabel;
Label2: TLabel;
cmbPrice: TComboBox;
edtPrice: TEdit;
Label3: TLabel;
edtCount: TEdit;
Label4: TLabel;
Label5: TLabel;
Bevel1: TBevel;
procedure FormCreate(Sender: TObject);
procedure btnCheckClick(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure btnExitClick(Sender: TObject);
procedure RadioGroup1Click(Sender: TObject);
private
FSeasonStrategy:TSaleStrategy;
FVIPStrategy:TSaleStrategy;
FTeamStrategy:TSaleStrategy;
FPriceSys:TPriceContext;
public
{ Public declarations }
end;
var
Client: TClIEnt;
implementation
{$R *.dfm}
procedure TClIEnt.FormCreate(Sender: TObject);
begin
FSeasonStrategy:=TSeasonStrategy.Create;
FVIPStrategy:=TVIPStrategy.Create;
FTeamStrategy:=TTeamStrategy.Create;
FPriceSys:=TPriceContext.Create;
end;
procedure TClIEnt.btnCheckClick(Sender: TObject);
var
i:integer;
price:Currency;
begin
case RadioGroup1.ItemIndex of
0:begin
FPriceSys.Strategy:=FSeasonStrategy ;
i:=MonthOf(dtpDate.DateTime);
end;
1:begin
FPriceSys.Strategy:=FVIPStrategy ;
i:=cmbVIP.ItemIndex;
end;
2:begin
FPriceSys.Strategy:=FTeamStrategy ;
i:=StrToInt(edtCount.Text);
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.Free;
FVIPStrategy.Free;
FTeamStrategy.Free;
end;
procedure TClIEnt.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:dtpDate.Enabled:=true;
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之間采取緊耦合的方法。