Delphi模式编程之策略模式(续)
刘 艺
1.3 策略模式在酒店管理系统中的应用
在酒店管理系统中,通常客房的价格不是一成不变的。对于住宿的淡季和旺季、老客户和新客户、散客和团队,都应该有不同的销售策略。显然,销售策略决定了报价。但是基于销售策略的报价体系又不能绑定于某一具体的客户端,因为只有把基于销售策略的报价体系独立出来,才能保证其重用性和可维护性。比如:一种报价体系一方面满足了优惠房价查询、客房结算等多个客户端的使用,另一方面又满足了不断调整的新销售策略的需求,这才算真正做到了重用性和可维护性。
对于以上的设计要求,选用策略模式是最好不过了。策略模式能够让算法变化独立于使用它的客户端。范例程序是一个基于策略模式的优惠房价查询模块,它包括一个基于销售策略的报价体系和一个优惠房价查询界面。当然,优惠房价查询界面只是该报价体系的客户端之一,报价体系亦可被其他客户端使用。
优惠房价查询模块的设计如图 1‑6所示。它包括了:
· 销售策略类TSaleStrategy,它是具体销售策略类的抽象基类。
· 3个具体销售策略类:TV
ipStrategy (VIP卡销售策略)、TTeamStrategy (团队销售策略)、TSeasonStrategy(季节销售策略)。
· 报价类T
PRiceContext,它是该策略模式中的上下文,持有一个到TStrategy的引用。
· 客户端类TClient,它是一个窗体类,即房价查询的界面。
示例程序 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.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