设计模式探秘:行为型模式——掌控对象间的协作与通信

在软件开发中,对象并非孤立存在,它们需要相互通信、协作来完成复杂的任务。行为型设计模式 (Behavioral Design Patterns) 正是专注于对象间职责的分配和通信机制。它们关注的是“对象应该做什么”以及“如何做”,旨在提高系统的灵活性、可扩展性和可维护性,让对象间的交互更加清晰、解耦。

行为型模式可以分为两大类:

  1. 类行为模式: 使用继承在类间分配行为(如模板方法模式)。
  2. 对象行为模式: 使用组合或委托在对象间分配行为(如策略模式、观察者模式等)。这类模式更为常用和灵活。

今天,我们将深入探讨十一种经典的对象行为模式(以及一个重要的类行为模式),理解它们如何优雅地解决对象协作中的常见问题。


1. 责任链模式 (Chain of Responsibility Pattern)

  • 核心思想: “传递请求”。避免请求发送者与接收者耦合,让多个对象都有机会处理请求,将这些对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它。
  • 解决的问题: 当有多个对象可以处理一个请求,但具体由哪个对象处理在运行时决定,或者希望将请求的发送者和接收者解耦。
  • 类比: 公司的报销流程。你(请求发送者)提交报销单,它先到你的直属经理,如果金额小经理可以批准;如果金额大,经理处理不了,就转交给部门总监;如果金额更大,总监再转交给财务总监。每个处理者(Handler)要么处理请求,要么将其传递给链上的下一个处理者。
  • 实现方式: 定义一个 Handler 接口,声明处理请求的方法和设置后继者(setSuccessor)的方法。具体的处理者(ConcreteHandler)实现 Handler 接口,在其处理方法中,如果能处理请求就处理,否则将请求转发给其后继者。客户端将请求发送给链上的第一个处理者。
  • 典型场景: 审批流程、异常处理、日志记录级别处理、事件在UI控件树中的传递。

2. 命令模式 (Command Pattern)

  • 核心思想: “请求即对象”。将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。
  • 解决的问题: 需要参数化对象以执行操作、在不同时间点安排请求的执行、支持操作的撤销/重做、将操作记录到日志中、支持事务性操作等。
  • 类比: 餐厅点餐。你(客户端)向服务员(调用者/Invoker)下达点餐命令(如“来一份宫保鸡丁”)。服务员不直接做菜,而是将这个命令(封装成一个Order对象)交给厨师(接收者/Receiver)去执行。这个Order对象包含了所有必要信息(菜品、数量)。你可以随时要求撤销这个命令(“不要了”),服务员可以将命令排队。
  • 实现方式: 定义一个 Command 接口,声明执行(execute())和可能的撤销(undo())方法。具体的命令类(ConcreteCommand)实现 Command 接口,并持有一个对具体接收者对象的引用。调用者(Invoker)持有 Command 对象的引用,并在其需要时调用 Commandexecute() 方法。接收者(Receiver)知道如何执行具体的操作。
  • 典型场景: GUI按钮/菜单命令、支持撤销/重做的操作(文本编辑器、绘图软件)、任务队列、宏命令、远程方法调用(RMI)。

3. 解释器模式 (Interpreter Pattern)

  • 核心思想: “定义语言的文法”。给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
  • 解决的问题: 当有一个简单的语言需要解释执行,且文法相对简单稳定时。
  • 类比: 计算器。表达式 “3 + 4 * 2” 是一个句子。解释器模式会将这个表达式解析成一个抽象语法树(AST),其中叶子节点是数字(终结符表达式),非叶子节点是操作符(非终结符表达式,如加法、乘法)。解释器从树的根节点开始,递归地解释每个子树,最终计算出结果。
  • 实现方式: 定义一个抽象的 Expression 接口,声明一个解释(interpret())方法。终结符表达式(TerminalExpression)和非终结符表达式(NonterminalExpression)都实现 Expression 接口。非终结符表达式通常包含其他 Expression 对象作为子节点。客户端构建语法树并调用根节点的 interpret() 方法。
  • 典型场景: 简单的领域特定语言(DSL)、正则表达式引擎(简化版)、数值或布尔表达式求值。注意:对于复杂语言,通常使用专门的解析器生成工具(如ANTLR)更合适。

4. 迭代器模式 (Iterator Pattern)

  • 核心思想: “访问聚合而不暴露其内部”。提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露该对象的内部表示。
  • 解决的问题: 需要遍历一个聚合对象(如列表、集合、树)的元素,但不想暴露其底层数据结构(如数组、链表)。
  • 类比: 电视遥控器的“换台”按钮。你可以按“上一个”、“下一个”来浏览所有电视频道,但你不需要知道电视台列表是用数组存储还是链表存储的。遥控器(Iterator)为你提供了统一的遍历接口。
  • 实现方式: 定义一个 Iterator 接口,声明 hasNext()next() 等方法。聚合对象(Aggregate)实现一个方法(如 createIterator())来返回一个具体的 Iterator 对象。具体的迭代器(ConcreteIterator)知道如何遍历其关联的聚合对象。客户端使用 Iterator 接口来遍历元素,无需关心聚合对象的内部结构。
  • 典型场景: 遍历各种集合类(Java的 Iterator, Python的 __iter__/__next__)、遍历树或图结构、数据库结果集遍历。

5. 中介者模式 (Mediator Pattern)

  • 核心思想: “协调者”。用一个中介对象来封装一系列对象之间的交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
  • 解决的问题: 当一组对象之间存在复杂的相互引用和通信,导致系统结构混乱、难以理解和维护时(“网状结构”)。
  • 类比: 机场塔台。飞机(同事类)之间不能直接通信协调起飞和降落,否则会非常混乱。所有飞机都与塔台(中介者)通信。塔台接收所有飞机的请求,并根据全局信息协调它们的行动,飞机之间互不知情。
  • 实现方式: 定义一个 Mediator 接口,声明用于同事对象通信的方法。具体的中介者(ConcreteMediator)实现此接口,并知道所有同事对象(Colleague)的引用。同事对象持有对中介者的引用,当需要与其他同事通信时,不是直接调用,而是通过中介者。中介者在收到请求后,负责协调和转发给适当的同事。
  • 典型场景: GUI组件交互(对话框中的按钮、文本框、复选框通过对话框本身协调)、聊天室、多玩家游戏中的房间管理。

6. 备忘录模式 (Memento Pattern)

  • 核心思想: “保存和恢复状态”。在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
  • 解决的问题: 实现对象状态的保存和恢复,特别是用于撤销操作,同时不暴露对象的内部细节。
  • 类比: 游戏存档。你在游戏(发起者/Originator)中到达一个关键点,选择“保存游戏”。游戏创建一个存档文件(备忘录/Memento),里面记录了当前所有角色状态、地图位置等信息。这个存档文件被保存在游戏外部(管理者/Caretaker,如存档管理器)。当你“读取存档”时,游戏从存档文件中恢复状态。
  • 实现方式: Originator 类可以创建一个 Memento 对象(包含其内部状态),并可以使用 Memento 对象来恢复自身状态。Memento 对象通常只提供有限的接口,以保护其内部状态。Caretaker 负责保存和管理 Memento 对象,但它不能修改或检查 Memento 的内容。
  • 典型场景: 撤销/重做功能、事务回滚、游戏存档、对象状态快照。

7. 观察者模式 (Observer Pattern)

  • 核心思想: “发布-订阅”。定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
  • 解决的问题: 当一个对象(主题/Subject)的状态改变需要通知其他多个对象(观察者/Observer),且希望这些对象是松耦合的。
  • 类比: 新闻订阅。你(观察者)订阅了某个新闻频道(主题)。当频道发布新新闻(状态改变)时,它会自动将新闻推送给所有订阅者。你不需要轮询频道是否有新新闻,频道也不需要知道每个订阅者的具体细节。
  • 实现方式: Subject 接口声明 attach() (注册观察者)、detach() (移除观察者) 和 notify() (通知所有观察者) 方法。ConcreteSubject 维护一个观察者列表,当其状态改变时调用 notify()Observer 接口声明 update() 方法。ConcreteObserver 实现 update() 方法,当被通知时,从 Subject 获取最新状态并更新自身。
  • 典型场景: 模型-视图-控制器(MVC)架构中的视图更新、事件处理系统、分布式消息系统、股票价格通知。

8. 状态模式 (State Pattern)

  • 核心思想: “状态即对象”。允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
  • 解决的问题: 当一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为。避免使用庞大的条件语句(if/else 或 switch)来处理不同状态下的不同行为。
  • 类比: 电梯。电梯有“运行”、“停止”、“开门”、“关门”等状态。在“运行”状态下,按“开门”按钮无效;在“停止”状态下,按“开门”按钮会进入“开门”状态。状态模式将每个状态封装成一个独立的类(RunningState, OpeningState),Context(电梯)对象持有一个当前状态对象的引用。当状态改变时,Context 将行为委托给当前的状态对象。
  • 实现方式: 定义一个 State 接口,声明与状态相关的行为方法。每个具体状态(ConcreteStateA, ConcreteStateB)实现 State 接口,并定义在该状态下对象的行为。Context 类持有一个 State 对象的引用,当状态改变时,Context 将自身引用传递给新的状态对象,并更新其状态引用。Context 的行为方法会委托给当前状态对象的方法。
  • 典型场景: 有限状态机(FSM)、工作流引擎、游戏角色状态(空闲、行走、攻击)、网络连接状态(连接、断开、重连)。

9. 策略模式 (Strategy Pattern)

  • 核心思想: “算法即服务”。定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。此模式使得算法可独立于使用它的客户而变化。
  • 解决的问题: 当一个类有多个行为(算法),并且这些行为在运行时需要动态切换,或者希望避免使用条件语句来选择算法。
  • 类比: 出行方式。从A点到B点,你可以选择步行、骑车、开车或坐公交(不同的策略)。出行者(上下文/Context)可以根据当前情况(如距离、时间、天气)选择最合适的出行策略(Strategy)。更换策略非常容易,且各种出行方式的具体实现对出行者是透明的。
  • 实现方式: 定义一个 Strategy 接口,声明算法的公共接口。具体的策略类(ConcreteStrategyA, ConcreteStrategyB)实现 Strategy 接口,提供具体的算法实现。Context 类持有一个 Strategy 对象的引用,并通过调用 Strategy 接口的方法来执行算法。Context 可以在运行时设置或更换 Strategy
  • 典型场景: 排序算法选择、支付方式(支付宝、微信、银行卡)、压缩算法、路径规划、缓存淘汰策略。

10. 模板方法模式 (Template Method Pattern)

  • 核心思想: “骨架与钩子”。定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变算法的结构即可重定义该算法的某些特定步骤。
  • 解决的问题: 当多个子类有共同的方法结构,但某些步骤的实现细节不同时。避免在每个子类中重复编写相同的代码框架。
  • 类比: 制作咖啡和茶的流程。两者都有相似的骨架:烧水 -> 冲泡 -> 倒入杯中 -> 加调料。但“冲泡”和“加调料”的具体步骤不同(咖啡用咖啡粉冲泡,加糖/奶;茶用茶叶冲泡,可能加柠檬)。模板方法定义了这个骨架,将“冲泡”和“加调料”作为抽象方法或钩子方法留给子类实现。
  • 实现方式: 在一个抽象类(AbstractClass)中定义一个模板方法(templateMethod()),该方法是final的,定义了算法的骨架,调用一系列基本方法(primitiveOperation())。这些基本方法可以是抽象的(必须由子类实现)、具体的(提供默认实现)或钩子方法(hookMethod(),空实现,子类可选择性覆盖)。具体的子类(ConcreteClass)继承抽象类并实现抽象的基本方法。
  • 典型场景: 框架设计(如Servlet的 doGet/doPost)、代码生成器、构建流程、数据处理流程。

11. 访问者模式 (Visitor Pattern)

  • 核心思想: “双重分派”。表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
  • 解决的问题: 当需要对一个复杂的对象结构(如组合模式中的树)中的元素执行多种不同的操作,且希望将这些操作与元素类本身解耦。避免在每个元素类中添加大量与特定操作相关的代码。
  • 类比: 一个公司组织结构图(树)。你需要对这个结构执行多种操作:计算总工资、打印组织架构、进行绩效评估。访问者模式允许你创建不同的“访问者”(PayrollVisitor, StructurePrinterVisitor, PerformanceEvaluatorVisitor),每个访问者实现对不同元素(Employee, Manager, Department)的特定操作。元素类只需提供一个 accept(Visitor) 方法,将自己“接受”给访问者。
  • 实现方式: 定义一个 Visitor 接口,为对象结构中的每种元素类型声明一个 visit() 方法。具体的访问者(ConcreteVisitor)实现 Visitor 接口,提供对每种元素的具体操作。元素接口(Element)声明一个 accept(Visitor) 方法。具体的元素类(ConcreteElement)实现 accept(),在其中调用访问者的 visit(this) 方法(this 是具体元素类型,实现双重分派)。对象结构(如 ObjectStructure)负责遍历所有元素并调用它们的 accept() 方法。
  • 典型场景: 编译器中的语法树遍历(生成代码、类型检查)、文档对象模型(DOM)操作、复杂数据结构的报表生成。注意:此模式相对复杂,且违背了“开闭原则”(添加新元素类型需要修改所有访问者),使用需谨慎。

总结:何时选择哪种行为型模式?

模式关键问题核心机制
责任链多个对象可能处理请求请求沿链传递
命令参数化、队列化、撤销操作请求封装为对象
解释器解释简单语言构建语法树并解释
迭代器遍历聚合对象提供统一遍历接口
中介者对象间交互复杂混乱引入协调中心
备忘录保存/恢复对象状态捕获并存储内部状态
观察者状态改变需通知多方发布-订阅机制
状态行为随状态改变状态封装为独立对象
策略算法需要动态切换算法封装为可替换对象
模板方法算法骨架固定,步骤可变父类定义骨架,子类实现步骤
访问者对结构元素执行多种新操作操作与元素解耦
Logo

惟楚有才,于斯为盛。欢迎来到长沙!!! 茶颜悦色、臭豆腐、CSDN和你一个都不能少~

更多推荐