一、行为型模式:让对象“动”起来的智慧
当我们用Pascal(这里以Object Pascal/Delphi为例)构建复杂系统时,常常会遇到这样的烦恼:对象之间你调用我,我调用你,关系乱成一团麻。系统功能每增加一点,代码就要大动干戈,牵一发而动全身。这时候,我们就需要一些“设计智慧”来让对象们既能协同工作,又能保持清爽的关系。
行为型模式,就是专门处理这类问题的“工具箱”。它不关心对象怎么创建(那是创建型模式的活儿),也不操心对象和谁组合(那是结构型模式的事儿),它聚焦于对象之间如何通信、如何分配职责、以及算法如何运行。简单说,它就是让对象们“聪明”地互动起来,而不是硬编码一堆复杂的调用逻辑。
在复杂的业务系统、图形界面应用或者游戏开发中,行为型模式就像一位经验丰富的交通指挥官,让数据流和控制流井然有序。接下来,我们通过几个最常用、最核心的模式,看看它们如何在实际的Pascal代码中大显身手。
二、观察者模式:建立一对多的“消息广播站”
想象一下,你有一个核心数据对象(比如一份订单的状态),它的状态一旦改变,需要立刻通知到用户界面、日志系统、库存管理系统等一大堆其他对象。如果让这个数据对象去挨个调用所有关心它的对象,那它的代码将变得无比臃肿,而且每增加一个关心者,都要修改它的代码。
观察者模式完美解决了这个问题。它定义了一种“订阅-发布”机制。核心对象(称为“主题”或“发布者”)只负责维护一个订阅者列表。当自身状态改变时,它不需要知道具体是谁在听,只需向列表里的所有“观察者”广播一条消息:“我变了!”。观察者们收到消息后,各自去取需要的数据并更新自己。
技术栈:Object Pascal (Delphi)
// 首先,定义观察者的通用接口。任何想监听变化的对象都必须实现这个接口。
type
IObserver = interface
['{GUID}'] // 一个唯一的标识符
procedure Update(Subject: TObject); // 当主题变化时,会被调用的方法
end;
// 然后,定义主题(被观察者)的基类。它负责管理观察者列表。
type
TSubject = class
private
FObservers: TList<IObserver>; // 存储所有观察者的列表
public
constructor Create;
destructor Destroy; override;
procedure Attach(Observer: IObserver); // 订阅:添加观察者
procedure Detach(Observer: IObserver); // 取消订阅:移除观察者
procedure Notify; // 通知:告诉所有观察者“我变了”
end;
TConcreteSubject = class(TSubject)
private
FState: Integer; // 主题内部的重要状态
public
property State: Integer read FState write SetState; // 属性设置器会触发通知
procedure SetState(AValue: Integer);
end;
// --- 实现部分 ---
constructor TSubject.Create;
begin
inherited;
FObservers := TList<IObserver>.Create;
end;
destructor TSubject.Destroy;
begin
FObservers.Free;
inherited;
end;
procedure TSubject.Attach(Observer: IObserver);
begin
if FObservers.IndexOf(Observer) < 0 then
FObservers.Add(Observer);
end;
procedure TSubject.Detach(Observer: IObserver);
begin
FObservers.Remove(Observer);
end;
procedure TSubject.Notify;
var
Obs: IObserver;
begin
for Obs in FObservers do
Obs.Update(Self); // 遍历列表,调用每个观察者的Update方法
end;
procedure TConcreteSubject.SetState(AValue: Integer);
begin
if FState <> AValue then
begin
FState := AValue;
Notify; // 状态改变,立即通知所有观察者!
end;
end;
// --- 具体观察者示例:一个日志记录器 ---
type
TLoggerObserver = class(TInterfacedObject, IObserver)
public
procedure Update(Subject: TObject);
end;
procedure TLoggerObserver.Update(Subject: TObject);
var
Subj: TConcreteSubject;
begin
if Subject is TConcreteSubject then
begin
Subj := TConcreteSubject(Subject);
WriteLn(Format('日志:主题状态已更新为 %d', [Subj.State])); // 执行自己的逻辑
end;
end;
// --- 使用示例 ---
var
OrderSubject: TConcreteSubject;
Logger, UIUpdater: IObserver; // 可以声明多个不同类型的观察者
begin
OrderSubject := TConcreteSubject.Create;
try
Logger := TLoggerObserver.Create;
// UIUpdater := TUIUpdaterObserver.Create; // 假设还有UI更新观察者
OrderSubject.Attach(Logger); // 日志系统订阅订单变化
// OrderSubject.Attach(UIUpdater); // UI系统订阅订单变化
WriteLn('改变订单状态...');
OrderSubject.State := 10; // 这里会自动触发 Notify,Logger会打印日志
// OrderSubject.Detach(Logger); // 如果不再需要,可以取消订阅
finally
OrderSubject.Free;
end;
end.
应用场景:GUI事件处理(如按钮点击通知多个控件)、数据模型与视图的同步(MVC架构)、实时数据监控系统。在Delphi中,VCL框架本身的事件机制就是观察者模式的典型体现。
优缺点:
- 优点:实现了主题和观察者的松耦合。主题不知道观察者的具体细节,反之亦然。可以随时增加或删除观察者,符合开闭原则。
- 缺点:如果观察者数量巨大,或者更新逻辑复杂,广播通知可能会成为性能瓶颈。另外,观察者需要自己从主题中拉取(pull)所需数据,如果传递的数据不恰当,可能导致观察者之间不必要的依赖。
注意事项:要小心循环引用(特别是在Delphi的接口和对象混用时)。确保在主题销毁前,观察者被正确移除。对于简单的场景,Delphi自带的TObservable和TObserver泛型类可以简化实现。
三、策略模式:把算法装进“插件”,随时替换
假如我们正在开发一个数据导出模块,需要支持导出为TXT、CSV、JSON等多种格式。最笨的办法是用一个巨大的case语句,根据用户选择调用不同的导出函数。这导致导出类职责过重,且增加一种新格式就要修改这个类的源代码。
策略模式告诉我们:将算法(策略)抽象出来,封装成独立的类,使得它们可以相互替换。这样,使用算法的上下文类(Context)就不再依赖具体的算法实现,而是依赖一个抽象的策略接口。
技术栈:Object Pascal (Delphi)
// 定义策略的通用接口
type
IExportStrategy = interface
procedure ExportData(const AData: TStrings); // 所有导出算法都必须实现这个导出方法
end;
// 实现具体的策略:TXT导出
type
TTextExportStrategy = class(TInterfacedObject, IExportStrategy)
public
procedure ExportData(const AData: TStrings);
end;
procedure TTextExportStrategy.ExportData(const AData: TStrings);
var
i: Integer;
begin
WriteLn('--- 开始导出为TXT文件 ---');
for i := 0 to AData.Count - 1 do
WriteLn(AData[i]); // 模拟写入文件
WriteLn('TXT导出完成。');
end;
// 实现具体的策略:JSON导出
type
TJsonExportStrategy = class(TInterfacedObject, IExportStrategy)
public
procedure ExportData(const AData: TStrings);
end;
procedure TJsonExportStrategy.ExportData(const AData: TStrings);
begin
WriteLn('--- 开始导出为JSON文件 ---');
WriteLn('{');
WriteLn(' "data": [');
// 这里简化处理,实际需要将AData格式化为JSON数组
WriteLn(' "' + StringReplace(AData.Text, sLineBreak, '","', [rfReplaceAll]) + '"');
WriteLn(' ]');
WriteLn('}');
WriteLn('JSON导出完成。');
end;
// 上下文类:它使用策略,但并不关心具体是哪种策略
type
TDataExporter = class
private
FStrategy: IExportStrategy; // 持有一个策略接口的引用
FData: TStrings;
public
constructor Create(const AData: TStrings);
destructor Destroy; override;
procedure SetStrategy(AStrategy: IExportStrategy); // 动态设置策略
procedure ExecuteExport; // 执行导出,委托给具体的策略对象
end;
constructor TDataExporter.Create(const AData: TStrings);
begin
inherited Create;
FData := TStringList.Create;
FData.Assign(AData);
end;
destructor TDataExporter.Destroy;
begin
FData.Free;
inherited;
end;
procedure TDataExporter.SetStrategy(AStrategy: IExportStrategy);
begin
FStrategy := AStrategy; // 切换策略就像换一个“插件”一样简单
end;
procedure TDataExporter.ExecuteExport;
begin
if Assigned(FStrategy) then
FStrategy.ExportData(FData) // 关键调用:委托
else
WriteLn('错误:未设置导出策略!');
end;
// --- 使用示例 ---
var
MyData: TStrings;
Exporter: TDataExporter;
begin
MyData := TStringList.Create;
try
MyData.Add('张三,30,工程师');
MyData.Add('李四,25,设计师');
Exporter := TDataExporter.Create(MyData);
try
// 使用TXT策略
Exporter.SetStrategy(TTextExportStrategy.Create);
Exporter.ExecuteExport;
WriteLn(''); // 换行
// 动态切换到JSON策略,无需修改TDataExporter的任何代码!
Exporter.SetStrategy(TJsonExportStrategy.Create);
Exporter.ExecuteExport;
// 未来增加XML导出,只需新建一个 TXmlExportStrategy 类即可。
finally
Exporter.Free;
end;
finally
MyData.Free;
end;
end.
应用场景:多种算法变体(如排序、压缩、加密算法)、支付方式选择(支付宝、微信、银联)、游戏中的角色技能或AI行为。
优缺点:
- 优点:完美符合开闭原则,新增策略无需修改上下文。消除了复杂的条件判断语句。算法可以自由复用和替换。
- 缺点:客户端必须了解所有策略的区别,以便选择合适的策略。如果策略很少且稳定,可能会增加系统里类的数量。
注意事项:策略对象通常是无状态的,它们只是提供算法。如果策略需要访问上下文的大量数据,可以考虑将上下文自身作为参数传递给策略方法。
四、状态模式:当对象拥有“多重人格”
考虑一个网络连接对象,它有“已连接”、“正在连接”、“已断开”等状态。在不同状态下,SendData()或Connect()方法的行为完全不同。传统的实现会用大量的if-else或case语句检查当前状态,然后执行对应代码。这导致方法冗长,且状态转换逻辑散落在各处,难以维护。
状态模式提供了一个优雅的解决方案:允许一个对象在其内部状态改变时改变它的行为,看起来像是改变了它的类。我们将每个状态都封装成一个独立的类,并将行为委托给当前的状态对象。
技术栈:Object Pascal (Delphi)
// 首先,定义状态接口,声明所有状态类需要实现的方法
type
ITCPConnectionState = interface
procedure Open(Connection: TObject);
procedure Close(Connection: TObject);
procedure Send(Connection: TObject; const Data: string);
end;
// 上下文类:TCP连接,它持有一个当前状态的引用
type
TTCPConnection = class
private
FState: ITCPConnectionState; // 当前状态
public
constructor Create;
procedure SetState(AState: ITCPConnectionState);
procedure RequestOpen;
procedure RequestClose;
procedure RequestSend(const Data: string);
// 其他方法...
end;
// --- 具体状态类:关闭状态 ---
type
TClosedState = class(TInterfacedObject, ITCPConnectionState)
public
procedure Open(Connection: TObject);
procedure Close(Connection: TObject);
procedure Send(Connection: TObject; const Data: string);
end;
procedure TClosedState.Open(Connection: TObject);
var
Conn: TTCPConnection;
begin
if Connection is TTCPConnection then
begin
Conn := TTCPConnection(Connection);
WriteLn('状态[关闭] -> 正在尝试连接...');
// 模拟连接过程...
WriteLn('连接成功!');
Conn.SetState(TConnectedState.Create); // 关键:状态转换,切换到“已连接”状态
end;
end;
procedure TClosedState.Close(Connection: TObject);
begin
WriteLn('状态[关闭] -> 已经是关闭状态,无需操作。');
end;
procedure TClosedState.Send(Connection: TObject; const Data: string);
begin
WriteLn('状态[关闭] -> 错误:连接未打开,无法发送数据。');
end;
// --- 具体状态类:已连接状态 ---
type
TConnectedState = class(TInterfacedObject, ITCPConnectionState)
public
procedure Open(Connection: TObject);
procedure Close(Connection: TObject);
procedure Send(Connection: TObject; const Data: string);
end;
procedure TConnectedState.Open(Connection: TObject);
begin
WriteLn('状态[已连接] -> 已经连接,无需重复打开。');
end;
procedure TConnectedState.Close(Connection: TObject);
var
Conn: TTCPConnection;
begin
if Connection is TTCPConnection then
begin
Conn := TTCPConnection(Connection);
WriteLn('状态[已连接] -> 正在关闭连接...');
// 模拟关闭过程...
WriteLn('连接已关闭。');
Conn.SetState(TClosedState.Create); // 关键:状态转换,切换回“关闭”状态
end;
end;
procedure TConnectedState.Send(Connection: TObject; const Data: string);
begin
WriteLn(Format('状态[已连接] -> 成功发送数据:[%s]', [Data])); // 正常发送
end;
// --- TTCPConnection 的实现 ---
constructor TTCPConnection.Create;
begin
inherited;
FState := TClosedState.Create; // 初始状态为“关闭”
end;
procedure TTCPConnection.SetState(AState: ITCPConnectionState);
begin
FState := AState;
end;
procedure TTCPConnection.RequestOpen;
begin
FState.Open(Self); // 委托给当前状态对象处理
end;
procedure TTCPConnection.RequestClose;
begin
FState.Close(Self); // 委托给当前状态对象处理
end;
procedure TTCPConnection.RequestSend(const Data: string);
begin
FState.Send(Self, Data); // 委托给当前状态对象处理
end;
// --- 使用示例 ---
var
Conn: TTCPConnection;
begin
Conn := TTCPConnection.Create;
try
Conn.RequestSend('Hello'); // 输出:错误:连接未打开,无法发送数据。
Conn.RequestOpen; // 输出:状态[关闭] -> 正在尝试连接... 连接成功!
Conn.RequestSend('Hello'); // 输出:状态[已连接] -> 成功发送数据:[Hello]
Conn.RequestOpen; // 输出:状态[已连接] -> 已经连接,无需重复打开。
Conn.RequestClose; // 输出:状态[已连接] -> 正在关闭连接... 连接已关闭。
finally
Conn.Free;
end;
end.
应用场景:工作流引擎(如订单状态:待支付、已支付、发货中、已完成)、游戏角色状态(站立、奔跑、跳跃、攻击)、UI控件状态(正常、禁用、悬停、按下)。
优缺点:
- 优点:将与特定状态相关的行为局部化到对应的状态类中,代码清晰。状态转换逻辑也封装在状态类内部或上下文中,易于管理和追踪。符合单一职责和开闭原则。
- 缺点:对于状态数量很少且简单的场景,可能会显得过度设计。状态类之间可能会产生依赖。
注意事项:谁负责状态转换?可以在上下文类中(集中式),也可以在具体状态类中(分布式)。上例是分布式,转换逻辑更清晰。要确保状态转换是完整的,避免出现未知状态。
五、命令模式:将请求封装成“可操作的对象”
在开发一个图形编辑器时,我们需要实现撤销(Undo)和重做(Redo)功能。用户的操作(如移动图形、改变颜色)可能很复杂,并且需要被记录、排队、延迟执行甚至撤销。
命令模式的核心思想是:将一个请求封装成一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。简单说,就是把“做什么”和“谁来做”、“什么时候做”分离开。
技术栈:Object Pascal (Delphi)
// 抽象命令接口
type
ICommand = interface
procedure Execute; // 执行命令
procedure Undo; // 撤销命令
end;
// 接收者:真正执行操作的对象
type
TShape = class
private
FX, FY: Integer;
FColor: string;
public
constructor Create(AX, AY: Integer; AColor: string);
procedure MoveTo(ADeltaX, ADeltaY: Integer);
procedure ChangeColor(ANewColor: string);
procedure Draw; // 模拟绘制
property X: Integer read FX;
property Y: Integer read FY;
property Color: string read FColor;
end;
// 具体命令:移动图形
type
TMoveCommand = class(TInterfacedObject, ICommand)
private
FReceiver: TShape; // 命令的接收者
FDeltaX, FDeltaY: Integer;
FOldX, FOldY: Integer; // 用于撤销
public
constructor Create(AReceiver: TShape; ADeltaX, ADeltaY: Integer);
procedure Execute; override;
procedure Undo; override;
end;
constructor TMoveCommand.Create(AReceiver: TShape; ADeltaX, ADeltaY: Integer);
begin
inherited Create;
FReceiver := AReceiver;
FDeltaX := ADeltaX;
FDeltaY := ADeltaY;
FOldX := AReceiver.X;
FOldY := AReceiver.Y;
end;
procedure TMoveCommand.Execute;
begin
WriteLn(Format('执行命令:移动图形 (%d, %d)', [FDeltaX, FDeltaY]));
FReceiver.MoveTo(FDeltaX, FDeltaY);
FReceiver.Draw;
end;
procedure TMoveCommand.Undo;
begin
WriteLn(Format('撤销命令:移动图形回 (%d, %d)', [FOldX, FOldY]));
FReceiver.MoveTo(FOldX - FReceiver.X, FOldY - FReceiver.Y); // 移回原处
FReceiver.Draw;
end;
// 调用者:负责触发命令(如菜单项、按钮、宏录制器)
type
TInvoker = class
private
FCommandHistory: TStack<ICommand>; // 命令历史栈,用于撤销
FUndoneHistory: TStack<ICommand>; // 重做栈
public
constructor Create;
destructor Destroy; override;
procedure ExecuteCommand(ACommand: ICommand);
procedure Undo;
procedure Redo;
end;
constructor TInvoker.Create;
begin
inherited;
FCommandHistory := TStack<ICommand>.Create;
FUndoneHistory := TStack<ICommand>.Create;
end;
destructor TInvoker.Destroy;
begin
FCommandHistory.Free;
FUndoneHistory.Free;
inherited;
end;
procedure TInvoker.ExecuteCommand(ACommand: ICommand);
begin
ACommand.Execute;
FCommandHistory.Push(ACommand); // 记录到历史
FUndoneHistory.Clear; // 执行新命令后,清空重做栈
end;
procedure TInvoker.Undo;
var
Cmd: ICommand;
begin
if FCommandHistory.Count > 0 then
begin
Cmd := FCommandHistory.Pop;
Cmd.Undo;
FUndoneHistory.Push(Cmd); // 放入重做栈
end
else
WriteLn('没有可以撤销的命令了。');
end;
procedure TInvoker.Redo;
var
Cmd: ICommand;
begin
if FUndoneHistory.Count > 0 then
begin
Cmd := FUndoneHistory.Pop;
Cmd.Execute;
FCommandHistory.Push(Cmd); // 重新放入历史栈
end
else
WriteLn('没有可以重做的命令了。');
end;
// --- TShape 的实现 ---
constructor TShape.Create(AX, AY: Integer; AColor: string);
begin
FX := AX;
FY := AY;
FColor := AColor;
end;
procedure TShape.MoveTo(ADeltaX, ADeltaY: Integer);
begin
FX := FX + ADeltaX;
FY := FY + ADeltaY;
end;
procedure TShape.ChangeColor(ANewColor: string);
begin
FColor := ANewColor;
end;
procedure TShape.Draw;
begin
WriteLn(Format(' 绘制图形于位置 (%d, %d),颜色:%s', [X, Y, Color]));
end;
// --- 使用示例 ---
var
MyShape: TShape;
Invoker: TInvoker;
MoveCmd1, MoveCmd2: ICommand;
begin
MyShape := TShape.Create(10, 10, '红色');
Invoker := TInvoker.Create;
try
MyShape.Draw;
// 创建并执行移动命令1
MoveCmd1 := TMoveCommand.Create(MyShape, 5, 5);
Invoker.ExecuteCommand(MoveCmd1);
// 创建并执行移动命令2
MoveCmd2 := TMoveCommand.Create(MyShape, -2, 3);
Invoker.ExecuteCommand(MoveCmd2);
WriteLn('--- 执行撤销 ---');
Invoker.Undo; // 撤销 MoveCmd2
Invoker.Undo; // 撤销 MoveCmd1
WriteLn('--- 执行重做 ---');
Invoker.Redo; // 重做 MoveCmd1
Invoker.Redo; // 重做 MoveCmd2
finally
Invoker.Free;
MyShape.Free;
end;
end.
应用场景:GUI操作(按钮点击对应命令)、任务队列、事务处理、宏录制/回放、支持撤销/重做的应用。
优缺点:
- 优点:将调用操作的对象与知道如何执行操作的对象解耦。可以轻松地组合命令成宏命令。支持撤销和重做。新的命令可以很容易地加入系统。
- 缺点:可能会产生大量的具体命令类,增加系统复杂度。
注意事项:命令对象是否应该保存完整的接收者状态以实现撤销?这取决于需求,有时保存状态快照,有时保存逆操作(如TMoveCommand保存了旧位置)。对于资源消耗大的场景要谨慎。
六、总结与综合应用思考
通过以上四个模式的深入探讨,我们可以看到,行为型模式的核心价值在于管理复杂度和提高灵活性。在复杂的Pascal/Delphi系统中:
- 观察者模式建立了清晰、低耦合的事件通知机制,是MVC、数据绑定等架构的基石。
- 策略模式让算法成为可拔插的组件,使系统在面对多变需求时更加从容。
- 状态模式将对象繁杂的状态行为清晰地分门别类,让状态转换和状态逻辑一目了然。
- 命令模式则将请求本身对象化,为实现高级功能如事务、撤销、队列、日志打开了大门。
在实际项目中,这些模式往往不是孤立使用的。例如,一个图形编辑器(状态模式管理工具状态)可能使用命令模式(实现绘制、移动等操作及撤销)来响应用户输入,而界面上的多个面板(观察者模式)则观察文档模型的变化以更新自身。
选择与权衡:没有最好的模式,只有最合适的模式。在引入模式前,先问自己:代码的痛点是什么?是if-else太多(考虑策略或状态),是对象间调用太乱(考虑观察者或中介者),还是需要记录操作(考虑命令)?避免为了模式而模式,简单的需求用简单的代码解决永远是第一原则。
Pascal的实现特色:Object Pascal是一门强类型、面向对象的语言,其接口(Interface)机制是实现这些模式的利器,它提供了比抽象类更纯粹的抽象和更灵活的复用(一个类可以实现多个接口)。结合泛型容器(如TList<T>, TStack<T>),可以写出既安全又优雅的模式实现代码。
掌握行为型模式,意味着你不仅学会了几个“套路”,更获得了一种分解复杂交互、构建可维护、可扩展系统的思维方式。在下一个复杂Pascal项目来临之时,不妨尝试用这些模式来武装你的代码,你会发现,驾驭复杂性,原来可以如此优雅。
评论