用Delphi实现模板模式(Template Method模式)
作者 陈省
考虑现在需要写一个全自动洗衣机的洗衣过程模拟程序,考虑到不同品牌的洗衣机的实现差异很大的,所以先建立一个洗衣的基类,定义了纯虚的洗衣方法,不同品牌的洗衣机分别重载洗衣的方法来实现洗衣的过程,定义的模型如下:

其中THaierWasher和TCygnetWasher分别对应海尔和小天鹅的洗衣机。进一步分析洗衣过程,会发现洗衣其实是由加水,搅拌,放水,甩干等固定的步骤按固定的顺序构成的。每一台洗衣机不论品牌都是这样的步骤和顺序,有区别是放水时,各个洗衣机不同,搅拌时速度也会不同,放水时快慢有所不同,甩干时转速会不一样。也就是说洗衣的大的流程是比较固定的,但是在细节实现上是有区别的。
因为大的流程是一致的,因此应该将这些大的流程行为抽象到公共基类中以避免代码重复。修改后的类图示意如下:

修改后,Wash方法不再是虚方法,不允许被重载,这样就固定了洗衣的大的流程,而不同品牌的洗衣机则重载细节实现方法包括Water(加水),BeatUp(搅拌),OutFlow(排水), Dry(甩干)。
修改后的Wash方法就相当于模版方法,一次性的实现了洗衣算法的不变的部分,而将可变的部分如Dry, BeatUp等方法延迟到子类来实现,这种模式就叫模板模式。前面我们刚刚研究了策略模式,它的目的也是实现动态的算法,和模板方法的区别在于策略模式上使用对象组合的方式来实现的,而模版方法是使用类继承的方法来实现的。策略模式替换的是整个算法,而模板方法则是只改变算法的一部分,总体算法并不改变。
VCL中的模板方法
VCL中大量的使用了模板方法,最常见的就是TStream类了,TStream类的定义如下:
TStream = class(TObject)
private
function GetPosition: Int64;
procedure SetPosition(const Pos: Int64);
procedure SetSize64(const NewSize: Int64);
protected
function GetSize: Int64; virtual;
procedure SetSize(NewSize: Longint); overload; virtual;
procedure SetSize(const NewSize: Int64); overload; virtual;
public
function Read(var Buffer; Count: Longint): Longint; virtual; abstract;
function Write(const Buffer; Count: Longint): Longint; virtual; abstract;
function Seek(Offset: Longint; Origin: Word): Longint; overload; virtual;
function Seek(const Offset: Int64; Origin: TSeekOrigin): Int64; overload; virtual;
procedure ReadBuffer(var Buffer; Count: Longint);
procedure WriteBuffer(const Buffer; Count: Longint);
function CopyFrom(Source: TStream; Count: Int64): Int64;
function ReadComponent(Instance: TComponent): TComponent;
function ReadComponentRes(Instance: TComponent): TComponent;
procedure WriteComponent(Instance: TComponent);
procedure WriteComponentRes(const ResName: string; Instance: TComponent);
procedure WriteDescendent(Instance, Ancestor: TComponent);
procedure WriteDescendentRes(const ResName: string; Instance, Ancestor: TComponent);
procedure WriteResourceHeader(const ResName: string; out FixupInfo: Integer);
procedure FixupResourceHeader(FixupInfo: Integer);
procedure ReadResHeader;
property Position: Int64 read GetPosition write SetPosition;
property Size: Int64 read GetSize write SetSize64;
end;
其中Read,Write是根据不同的流类必须重载的方法,因此定义为Abstract方法,而Seek方法对于不同的类如THandleStream可能也需要重载,但是TStream类给了一个一般意义的默认实现,对于这类需要重载的方法,一般称其为钩子方法。而CopyFrom,WriteComponent等方法的算法都比较固定,因此在基类中算法都已经被实现了,这些方法都是模版方法。比如下面就是CopyFrom方法的算法,它调用ReadBuffer和WriteBuffer来完成流数据的复制:
function TStream.CopyFrom(Source: TStream; Count: Int64): Int64;
const
MaxBufSize = $F000;
var
BufSize, N: Integer;
Buffer: PChar;
begin
if Count = 0 then
begin
Source.Position := 0;
Count := Source.Size;
end;
Result := Count;
if Count > MaxBufSize then BufSize := MaxBufSize else BufSize := Count;
GetMem(Buffer, BufSize);
try
while Count <> 0 do
begin
if Count > BufSize then N := BufSize else N := Count;
Source.ReadBuffer(Buffer^, N);
WriteBuffer(Buffer^, N);
Dec(Count, N);
end;
finally
FreeMem(Buffer, BufSize);
end;
end;
其中ReadBuffer和WriteBuffer方法也是模板方法,其实是调用Read和Write虚方法来完成的,Read和Write方法被延迟到了子类来实现:
procedure TStream.ReadBuffer(var Buffer; Count: Longint);
begin
if (Count <> 0) and (Read(Buffer, Count) <> Count) then
raise EReadError.CreateRes(@SReadError);
end;
procedure TStream.WriteBuffer(const Buffer; Count: Longint);
begin
if (Count <> 0) and (Write(Buffer, Count) <> Count) then
raise EWriteError.CreateRes(@SWriteError);
end;
下面就是THandleStream对钩子方法的实现:
function THandleStream.Read(var Buffer; Count: Longint): Longint;
begin
Result := FileRead(FHandle, Buffer, Count);
if Result = -1 then Result := 0;
end;
function THandleStream.Write(const Buffer; Count: Longint): Longint;
begin
Result := FileWrite(FHandle, Buffer, Count);
if Result = -1 then Result := 0;
end;
function THandleStream.Seek(const Offset: Int64; Origin: TSeekOrigin): Int64;
begin
Result := FileSeek(FHandle, Offset, Ord(Origin));
end;
除了TStream之外,我们常用的TStrings以及TCustomIniFile等类也都是模板类。