Dephi状态模式(State模式)
作者 陈省
对于面向对象开发来说,很长的函数和过程是不容易维护的,同样冗长的if else 以及case of的条件判断语句会使得代码不够清晰,特别是当条件判断很多时,比如有几十条甚至十几条条件判断语句,会使得代码难以修改和维护。
在早期windows开发中,程序员经常需要编写巨大的消息判断语句,根据消息的不同调用不同的消息处理函数,后来为了改善消息处理的复杂程度,Delphi引入了message关键字,从编译器一级对消息处理进行了映射,减少了条件判断语句的大量使用,这一案例就是巨大的条件判断语句弊端的典型体现。
通常说来,if else 以及case 语句都是对对象的某个状态或者属性进行判断,根据对象的状态或属性的不同,执行不同的操作。实际上就是一个有限状态机,为了消除这些if else条件判断语句,我们可以使用State模式来解决。
所谓状态模式就是将宿主对象中每一种可能的状态抽象成一个状态类,当宿主对象的状态发生变化时,宿主对象改变自己的状态,并执行不同状态类对应的不同操作。以圣斗士星矢为例,当他的小宇宙处于不同的状态时,他发出的天马流星拳、天狗流星拳、天鹅流星拳具有不同的威力,分别对应于青铜圣斗士,白银圣斗士,黄金圣斗士的级别。对星矢进行建模后的UML图示意如下:

比较一下使用State模式前后的不同,没有使用State模式前,星矢使用天马流星拳等招数的伪代码:
procedure 星矢.天马流星拳;
begin
case 小宇宙 of
黄金圣斗士: 打出黄金圣斗士威力的天马流星拳…
白银圣斗士:打出白银圣斗士威力的天马流星拳…
青铜圣斗士:打出青铜圣斗士威力的天马流星拳…
end;
end;
procedure 星矢.天狗流星拳;
begin
case 小宇宙 of
黄金圣斗士: 打出黄金圣斗士威力的天狗流星拳…
白银圣斗士:打出白银圣斗士威力的天狗流星拳…
青铜圣斗士:打出青铜圣斗士威力的天狗流星拳…
end;
end;
procedure 星矢.天鹅流星拳;
begin
case 小宇宙 of
黄金圣斗士: 打出黄金圣斗士威力的天鹅流星拳…
白银圣斗士:打出白银圣斗士威力的天鹅流星拳…
青铜圣斗士:打出青铜圣斗士威力的天鹅流星拳…
end;
end;
使用State模式后,星矢使用天马流星拳等招数的伪代码示意如下:
//设定小宇宙状态
星矢.小宇宙:=黄金圣斗士;
procedure 星矢.天马流星拳;
begin
小宇宙.天马流星拳
end;
procedure 星矢.天狗流星拳;
begin
小宇宙.天狗流星拳
end;
procedure 星矢.天鹅流星拳;
begin
小宇宙.天鹅流星拳
end;
可以看到宿主对象的代码简化了许多,同时消除了case条件判断语句,同时使用State模式的好处还在于便于扩充和修改,比如要调整黄金圣斗士的天马流星拳的威力,只要修改黄金圣斗士小宇宙对象的相应方法就可以了,不用象以前那样在一个case语句中寻找相应的状态对应的代码。同时便于扩充,比如客户觉得有必要给星矢增加一个小宇宙状态,比如超级塞亚人小宇宙,在不改动宿主对象代码的情况,只要添加一个新类就可以了,宿主对象的所有同小宇宙相关的招数方法都无须改变,而使用case条件判断语句的话,则需要在每一个招数方法中添加新的判断,如果星矢会的招数太多的话,修改的地方就是大量的了。虽然State模式有上面的优点,但是它也有它的缺点,就是如果宿主对象的状态比较多的话,会产生大量的小粒度的对象,显得对象过多,体系不紧凑。
另外,State模式的模型图和策略模式的模型图几乎一样,区别在于状态模式的特点是状态会经常发生变化,而策略模式一般来说在使用的时候通常是固定的,不会频繁变化。另外两个模式的目的也不同,对状态改变时,对象具有不同的行为进行抽象,对应于State模式。而策略模式则是为了解决算法的互换问题的。
状态模式使用示例
在Delphi的\demos\doc\graphex目录下,提供了一个画图程序的例子,运行后可以根据当前绘图工具的不同,绘制不同的形状,示意图如下:

其中核心的绘图部分就是一个大的添加判断过程DrawShape,根据当前绘图工具的不同(线、圆、矩形、圆角矩形),绘制不同的形状,代码如下:
procedure TForm1.DrawShape(TopLeft, BottomRight: TPoint; AMode: TPenMode);
begin
with Image.Canvas do
begin
Pen.Mode := AMode;
case DrawingTool of
dtLine:
begin
Image.Canvas.MoveTo(TopLeft.X, TopLeft.Y);
Image.Canvas.LineTo(BottomRight.X, BottomRight.Y);
end;
dtRectangle: Image.Canvas.Rectangle(TopLeft.X, TopLeft.Y, BottomRight.X,
BottomRight.Y);
dtEllipse: Image.Canvas.Ellipse(Topleft.X, TopLeft.Y, BottomRight.X,
BottomRight.Y);
dtRoundRect: Image.Canvas.RoundRect(TopLeft.X, TopLeft.Y, BottomRight.X,
BottomRight.Y, (TopLeft.X - BottomRight.X) div 2,
(TopLeft.Y - BottomRight.Y) div 2);
end;
end;
end;
下面我们就对这一例子使用State模式进行改造,消除Case判断。首先对系统建模:

修改后的代码如下:
…
type
TDrawingTool = class(TObject)
private
FImage: TImage;
public
constructor Create(AImage: TImage);
procedure DrawShape(TopLeft, BottomRight: TPoint; AMode: TPenMode); virtual;
abstract;
end;
TLineTool = class(TDrawingTool)
procedure DrawShape(TopLeft, BottomRight: TPoint; AMode: TPenMode);
override;
end;
TRectTool = class(TDrawingTool)
procedure DrawShape(TopLeft, BottomRight: TPoint; AMode: TPenMode);
override;
end;
TRoundRectTool = class(TDrawingTool)
procedure DrawShape(TopLeft, BottomRight: TPoint; AMode: TPenMode);
override;
end;
TEllipseTool = class(TDrawingTool)
procedure DrawShape(TopLeft, BottomRight: TPoint; AMode: TPenMode);
override;
end;
TForm1 = class(TForm)
…
private
FDrawingTool: TDrawingTool;
procedure SetDrawingTool(const Value: TDrawingTool);
{ Private declarations }
public
{ Public declarations }
…
procedure DrawShape(TopLeft, BottomRight: TPoint; AMode: TPenMode);
property DrawingTool: TDrawingTool read FDrawingTool write SetDrawingTool;
end;
…
procedure TForm1.LineButtonClick(Sender: TObject);
begin
FreeAndNil(FDrawingTool);
DrawingTool := TLineTool.Create(Image);
end;
procedure TForm1.RectangleButtonClick(Sender: TObject);
begin
FreeAndNil(FDrawingTool);
DrawingTool := TRectTool.Create(Image);
end;
procedure TForm1.EllipseButtonClick(Sender: TObject);
begin
FreeAndNil(FDrawingTool);
DrawingTool := TEllipseTool.Create(Image);
end;
procedure TForm1.RoundRectButtonClick(Sender: TObject);
begin
FreeAndNil(FDrawingTool);
DrawingTool := TRoundRectTool.Create(Image);
end;
procedure TForm1.DrawShape(TopLeft, BottomRight: TPoint; AMode: TPenMode);
begin
DrawingTool.DrawShape(TopLeft, BottomRight, AMode);
end;
…
procedure TForm1.FormCreate(Sender: TObject);
…
begin
…
LineButton.OnClick(nil);
end;
…
{ TEllipseTool }
procedure TEllipseTool.DrawShape(TopLeft, BottomRight: TPoint;
AMode: TPenMode);
begin
with FImage.Canvas do
begin
Pen.Mode := AMode;
Ellipse(Topleft.X, TopLeft.Y, BottomRight.X,
BottomRight.Y);
end;
end;
{ TRoundRectTool }
procedure TRoundRectTool.DrawShape(TopLeft, BottomRight: TPoint;
AMode: TPenMode);
begin
with FImage.Canvas do
begin
Pen.Mode := AMode;
RoundRect(TopLeft.X, TopLeft.Y, BottomRight.X,
BottomRight.Y, (TopLeft.X - BottomRight.X) div 2,
(TopLeft.Y - BottomRight.Y) div 2);
end;
end;
{ TRectTool }
procedure TRectTool.DrawShape(TopLeft, BottomRight: TPoint;
AMode: TPenMode);
begin
with FImage.Canvas do
begin
Pen.Mode := AMode;
Rectangle(TopLeft.X, TopLeft.Y, BottomRight.X, BottomRight.Y);
end;
end;
{ TLineTool }
procedure TLineTool.DrawShape(TopLeft, BottomRight: TPoint;
AMode: TPenMode);
begin
with FImage.Canvas do
begin
Pen.Mode := AMode;
MoveTo(TopLeft.X, TopLeft.Y);
LineTo(BottomRight.X, BottomRight.Y);
end;
end;
procedure TForm1.SetDrawingTool(const Value: TDrawingTool);
begin
FDrawingTool := Value;
end;
{ TDrawingTool }
constructor TDrawingTool.Create(AImage: TImage);
begin
FImage := AImage;
end;
end.