一、行为型模式:让对象“动”起来的智慧

当我们用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自带的TObservableTObserver泛型类可以简化实现。

三、策略模式:把算法装进“插件”,随时替换

假如我们正在开发一个数据导出模块,需要支持导出为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-elsecase语句检查当前状态,然后执行对应代码。这导致方法冗长,且状态转换逻辑散落在各处,难以维护。

状态模式提供了一个优雅的解决方案:允许一个对象在其内部状态改变时改变它的行为,看起来像是改变了它的类。我们将每个状态都封装成一个独立的类,并将行为委托给当前的状态对象。

技术栈: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项目来临之时,不妨尝试用这些模式来武装你的代码,你会发现,驾驭复杂性,原来可以如此优雅。