设计模式与惯用法

共 22 题
📑 题目列表 22 题
#
★★★

1. Singleton 的工程取舍中进程级、classloader 级、容器级的生命周期反模式

讨论 Singleton 模式的工程取舍,以及进程级、classloader 级、容器级生命周期的反模式问题?

  • Singleton 的利弊
  • 生命周期级别问题
  • 反模式与替代

Singleton 确保全局唯一实例,但常被批评为反模式:隐藏依赖、难以测试、全局可变状态。生命周期问题:进程级 Singleton 在每个 JVM 进程各有一份,多进程部署下失去"全局唯一";classloader 级 Singleton 依赖 classloader 作用域,多个 classloader 加载同一类会产生多个实例,易出 bug;容器级 Singleton(如 Spring 单例)由 IoC 容器管理生命周期,作用域可控,是较健康的"单例"。工程取舍:慎用"手写 getInstance() 单例",尽量用容器管理单例(依赖注入 + 单例作用域)或静态无状态工具。反模式的核心是"全局可访问的可变状态 + 隐藏依赖"。

Singleton 的真正问题不是"唯一实例",而是"全局可访问 + 隐藏依赖 + 难测试"。容器级单例(DI 管理)保留单例特性又显式依赖,可测试。手写单例应避免携带可变状态。理解生命周期级别(进程/classloader/容器)是诊断单例 bug 的关键。

#
★★★

2. Factory Method 与 Abstract Factory 在框架扩展点的差异

对比 Factory Method 与 Abstract Factory 在框架扩展点上的差异?

  • 工厂方法模式
  • 抽象工厂模式
  • 框架扩展点应用

Factory Method(工厂方法):在基类中定义"创建对象"的抽象方法,子类决定实例化哪个具体类。它是"单个产品"的创建,通过子类覆写工厂方法实现扩展,是框架里的经典扩展点——基类定义模板逻辑,子类用工厂方法注入具体对象。Abstract Factory(抽象工厂):定义一个"创建一族相关产品"的接口,每个具体工厂创建一族对象。它是"一组成套产品"的创建,保证产品族的一致性。应用差异:Factory Method 用于"单个变化点"的扩展(如创建某个具体依赖),Abstract Factory 用于"对象族需配套"的场景(如 UI 主题,一整套组件风格一致)。框架里 Factory Method 是常用扩展点,Abstract Factory 是更大粒度的产品族封装。

Factory Method 是"单产品 + 子类决定",Abstract Factory 是"产品族 + 家族一致"。Factory Method 通过继承扩展,Abstract Factory 通过组合(工厂对象)扩展。框架扩展点多用 Factory Method(子类覆写),Abstract Factory 用于产品族协调。

#
★★★

3. Builder 的"director + builder + product"三角色与流式 API(fluent API)

解释 Builder 模式的"director + builder + product"三角色,以及流式 API(fluent API)?

  • Builder 三角色
  • 流式 API
  • 应用场景

Builder 模式分离"对象构造"与"对象表示",三角色:Product(被构建的复杂对象)、Builder(定义构建步骤的接口/抽象)、Director(编排构建步骤,也可省略,由客户端直接调用 Builder)。Builder 提供分步设置属性,最终 build() 产出 Product。流式 API(fluent API):Builder 的每个设置方法返回 this,形成链式调用(如 new Order.Builder().id(1).price(100).build()),可读性好、参数顺序自由、避免冗长构造器。应用:对象构造参数多、可选参数多、构造复杂。Java 的 Lombok @Builder、Effective Java 的 Builder、Kotlin 的 data class copy 都体现此思想。Director 在有固定构建流程时使用,否则客户端直接调 Builder。

Builder 的核心是"把构造过程中的参数设置与最终构建分离",解决"多参数构造器(telescoping constructor)"问题。流式 API 提升可读性。Director 有助于复用固定构建流程。Builder 是工厂模式的补充,用于构造复杂对象。

#
★★★

4. Adapter 的类适配 vs 对象适配与遗留系统兼容

Adapter 模式的类适配与对象适配有何区别?如何用于遗留系统兼容?

  • 类适配 vs 对象适配
  • 遗留系统兼容
  • 取舍

Adapter(适配器)把不兼容的接口转换为客户端需要的接口。类适配器用继承:适配器继承被适配类并实现目标接口,复用被适配类的方法(Java 单继承下受限);对象适配器用组合:适配器持有被适配对象并实现目标接口,委托给被适配对象(更推荐,灵活性高、不依赖继承)。遗留系统兼容:当遗留系统接口与新的客户端接口不一致时,用 Adapter 包装遗留对象,把旧接口映射到新接口,客户端只依赖新接口,无需修改遗留代码。对象适配更优(解耦、可适配多个对象、不破坏封装),类适配在需要覆写被适配类方法时有用。这是"组合优于继承"的又一体现。

类适配用继承、对象适配用组合。对象适配更灵活、更符合组合优先。遗留系统兼容是 Adapter 的典型场景:不修改遗留代码,用适配器桥接接口差异,实现平滑迁移。它也是防腐层的基础。

#
★★★

5. Bridge 的"抽象与实现分离"在 JDBC 驱动的应用

Bridge 模式如何实现"抽象与实现分离"?在 JDBC 驱动中如何应用?

  • Bridge 模式
  • 抽象与实现分离
  • JDBC 应用

Bridge(桥接)把"抽象部分"与"实现部分"分离,让两者可独立变化,通过组合连接。抽象定义高层接口,实现是可替换的底层实现,二者通过"桥"(持有的实现引用)连接。JDBC 是经典应用:JDBC API(DriverManager、Connection、Statement)是抽象,具体的数据库驱动(MySQL、Oracle、PostgreSQL 驱动)是实现。应用代码只依赖 JDBC 抽象接口,实际连接由 DriverManager 加载具体驱动实现。抽象(统一 SQL API)与实现(各数据库驱动)可独立演化——新增数据库只加驱动,不改抽象。

Bridge 与 Adapter 类似但目的不同:Bridge 是"设计时分离抽象与实现"让两者独立变化,Adapter 是"适配既有不兼容接口"。JDBC 用 Bridge 让数据库抽象与驱动实现解耦,实现"换数据库不改代码"。这是 DIP 的体现。

#
★★★

6. Composite 的树形结构在 AST、组织结构、目录树的应用

Composite 模式如何表示树形结构?在 AST、组织结构、目录树中如何应用?

  • Composite 模式
  • 树形结构
  • 应用场景

Composite(组合)把对象组织成树形结构,让"部分"与"整体"统一处理:叶子节点(Leaf)与复合节点(Composite)实现同一接口,复合节点持有子节点列表,客户端可以一致地对待叶子和复合节点。应用:AST(抽象语法树)——表达式、语句既是叶子又是复合节点,统一用 Visitor 处理;组织结构——部门包含员工与子部门,可递归计算人数/成本;目录树——文件是叶子、目录是复合节点,可递归遍历/计算大小。Composite 用"递归结构"统一了部分与整体,客户端无需区分叶子与复合,简化了树形数据操作。

Composite 的核心是"叶子与复合节点统一接口",让客户端递归处理树,无需 if 判断节点类型。它天然适合任何"部分-整体"层级结构(AST、组织、目录、UI 组件树)。是处理树形数据的经典模式。

#
★★★

7. Facade 的子系统封装与 API Gateway 模式对比

Facade 模式的子系统封装与 API Gateway(网关)模式有何对比?

  • Facade 模式
  • API Gateway
  • 两者的对比

Facade(门面)为复杂的子系统提供统一、简化的入口接口,隐藏内部子系统的复杂性,客户端通过门面访问多个子系统,减少耦合。API Gateway(网关)是微服务架构中的统一入口,对客户端屏蔽多个后端服务的细节,提供路由、聚合、鉴权、限流等能力。对比:Facade 是"类/模块级"的门面(进程内,封装子系统),API Gateway 是"服务级"的门面(网络边界,聚合多个服务)。两者思想相同——"统一入口 + 隐藏复杂性 + 客户端解耦",但粒度与实现不同:Facade 是代码模式,Gateway 是分布式架构组件。Facade 让客户端不直接依赖多个子系统,Gateway 让客户端不直接依赖多个微服务。

Facade 与 API Gateway 是同一思想在不同规模的表现:统一入口、隐藏复杂度、降低客户端耦合。Facade 在进程内、Gateway 在服务边界。理解这个类比有助于在不同架构层运用"门面"思想。

#
★★★

8. Flyweight 在字符、icon、tree 节点的内部状态共享

Flyweight 模式如何通过内部状态共享节省内存?在字符、icon、tree 节点中如何应用?

  • Flyweight 模式
  • 内部状态与外部状态
  • 应用场景

Flyweight(享元)通过共享"内部状态"(不可变、可共享的部分)来节省内存,把"外部状态"(可变、随上下文变化的部分)移出对象。例如字符渲染:字形(Glyph)是内部状态可共享,而字符的位置、大小是外部状态,渲染时临时传入。又如 icon:图标本身(内部状态)可共享,其在界面中的位置是外部状态。tree 节点:树节点共享的元数据(如类型、图标)是内部状态,而节点值、父指针是外部状态。Flyweight 用工厂统一管理共享对象,大幅减少重复对象占用的内存。适用场景:大量相似对象、内存敏感(如文本编辑器、游戏粒子、图标系统)。

Flyweight 的核心是"区分内部/外部状态,共享内部、外置外部"。它通过对象共享降低内存,适用于"大量相似对象"的场景。工厂管理享元池,客户端传入外部状态。现代实现常用"共享不可变对象"(如缓存、常量池)体现此思想。

#
★★★

9. Chain of Responsibility 在 Servlet Filter、Spring Interceptor 的工程链

责任链模式(Chain of Responsibility)在 Servlet Filter、Spring Interceptor 中如何形成工程链?

  • 责任链模式
  • Servlet Filter 链
  • Spring Interceptor 链

Chain of Responsibility(责任链)把请求沿链传递,每个处理器决定"处理 or 传给下一个",解耦请求发送者与处理者。Servlet Filter 链:多个 Filter 组成过滤器链,请求依次经过每个 Filter(可继续传递或终止),实现鉴权、日志、编码等横切处理。Spring Interceptor 链:Interceptor 在 Controller 前后执行,多个 Interceptor 按顺序形成链,用于鉴权、日志、通用处理。两者是责任链在 Web 框架的工程化:通过配置声明多个处理器,框架按序调用,Handler 可 doFilter/next 放行或拦截。责任链让"新增处理环节"只需添加链节点,不改既有逻辑,符合 OCP。

Filter 与 Interceptor 都是责任链的框架实现。责任链解耦"处理环节"与"请求",支持插拔式扩展(新增 Filter/Interceptor 即扩展)。Filter 更底层(Servlet 级),Interceptor 在 Spring MVC 层。理解责任链有助于配置与扩展 Web 中间件。

#
★★★

10. 设计模式「过度设计」的识别,如何用「模式命名入代码」(pattern-in-the-name)与「万能模式滥用」(如 God Object + Singleton)两类反模式做评审门禁?

如何识别设计模式"过度设计"?用"模式命名入代码"与"万能模式滥用"两类反模式做评审门禁?

  • 过度设计的识别
  • 模式命名入代码反模式
  • 万能模式滥用

设计模式过度设计的两类反模式:一是"模式命名入代码"(pattern-in-the-name)——把模式名塞进类名或方法名(如 XxxFactory、XxxSingleton、XxxManager),但实际没有对应的模式结构或需求,为"用模式而用模式",是形式主义;二是"万能模式滥用"——用某个模式硬套所有问题(如 God Object + Singleton 把所有逻辑塞进一个全局单例),或为不需要的场景强行套模式(如为两个方法也建抽象工厂)。评审门禁:代码评审时检查"为什么用这个模式?"——模式必须有真实需求(多态、变化、复杂结构)支撑;禁止"为模式命名而命名";检查是否有"万能模式"(一个模式到处用);用"模式收益 vs 复杂度"评估。识别过度设计后,返回简化实现。

过度设计是"为模式而模式",而非"为问题而模式"。评审门禁的关键是"模式要有真实问题支撑":命名入代码是形式主义,万能滥用是教条。评估模式是否必要,看它解决的复杂度是否真实存在。这是 KISS 与设计模式的平衡。

#
★★

11. Command 的"动作对象化"与 Undo/Redo、队列延迟执行

Command 模式如何实现"动作对象化"?如何支持 Undo/Redo 与队列延迟执行?

  • Command 模式
  • 动作对象化
  • Undo/Redo 与队列

Command(命令)把"一个动作/请求"封装成对象,包含执行所需的信息(接收者、参数),使动作可被存储、传递、调度。核心是"动作对象化":把"调用"抽象成可传递的对象。由此可支持:Undo/Redo——每个命令对象记录执行与撤销(undo),通过维护命令历史栈,可回退/重做;队列延迟执行——命令对象可放入队列,延迟或异步执行(如任务队列、事务日志);批量/日志——命令可记录、重放。Command 把"发起者"与"执行者"解耦,动作可复用、可组合。

Command 的核心是"把动作变成数据(对象)",因此可存储、排队、撤销。Undo/Redo 靠命令的 undo 方法加历史栈,延迟执行靠队列;它解耦调用与执行,是"可撤销、可延迟"操作的基础。

#
★★

12. Interpreter 在 DSL、正则、SQL 解析的应用边界

Interpreter 模式在 DSL、正则、SQL 解析中的应用边界是什么?

  • Interpreter 模式
  • 应用场景
  • 适用边界

Interpreter(解释器)把一种语言/表达式的语法表示成对象层级,并解释执行。应用:DSL(领域特定语言)——自定义配置/规则表达式,用树形结构解释;正则表达式——正则引擎把正则编译成可解释的语法树匹配文本;SQL 解析——SQL 语句解析成语法树再执行。应用边界:Interpreter 适合"语法简单、变化频繁、表达式规模可控"的场景;当语法复杂、表达式数量巨大、性能敏感时,直接手写 Interpreter 会类爆炸、慢,应改用现成的解析器生成器(ANTLR)、正则引擎、或外部 DSL 库。Interpreter 适合"小型、可控的语法",大型复杂语言应务实选择现成工具。

Interpreter 的边界是"语法复杂度与性能"。小型 DSL 用 Interpreter 类结构清晰;复杂语法(SQL、完整语言)用解析器生成器更务实。判断:语法简单且可表达为小对象树 → Interpreter;复杂/高性能 → 现成解析工具。避免"为简单 DSL 造复杂解释器引擎"。

#
★★

13. Iterator 与 foreach 的外部迭代器 vs 内部迭代器

Iterator 与 foreach 的外部迭代器 vs 内部迭代器有何区别?

  • 外部迭代器
  • 内部迭代器
  • forEach 与迭代器

外部迭代器(external iterator):由客户端控制迭代过程,显式调用 hasNext()/next()(如 Java 的 Iterator、C++ 迭代器),客户端决定何时推进、何时停止。内部迭代器(internal iterator):由容器/函数控制迭代,客户端提供回调,框架遍历时调用(如 Java 的 forEach 回调、Ruby 的 each、函数式 map/filter)。区别:外部迭代器灵活(可控制、可暂停、可手动跳过),但代码冗长;内部迭代器简洁(声明式)、可隐藏遍历细节,但客户端不能中途随意控制(除非框架支持)。foreach 是外部迭代器的语法糖(Java 增强 for 底层是 Iterator),函数式 forEach 是内部迭代器。现代倾向内部迭代器(声明式、安全、易并行)。

外部 vs 内部迭代器是"客户端控制 vs 容器控制"的迭代。外部灵活、内部简洁。foreach 本质是外部迭代器语法糖,forEach 回调是内部迭代器。理解差异有助于选择迭代方式,函数式编程更倾向内部迭代器。

#
★★

14. Mediator 在多对象协作的"中介者"反场景风险

Mediator 模式在多对象协作中的"中介者"反场景风险是什么?

  • Mediator 模式
  • 中介者反场景
  • 风险控制

Mediator(中介者)用中介对象封装多对象之间的交互,让对象不直接互相引用,而是通过中介者通信,降低对象间耦合。但存在反场景风险:中介者退化为"上帝对象"(God Mediator)——所有交互逻辑都集中到中介者,中介者变得庞大、复杂、难以维护,成为新的耦合中心;对象之间失去直接沟通,导致间接调用链、可读性下降;中介者过重时,修改逻辑要改中介者,违背 SRP。风险控制:保持中介者职责单一(只做编排,不含过多业务);如果交互简单,不需要中介者(直接协作更清晰);避免把所有逻辑都塞进中介者。判断:对象多且交互复杂用 Mediator,对象少且交互简单则直接协作。

Mediator 的风险是"中介者肥胖"——把耦合从"多对多"转成"集中到中介者",可能变成新的上帝对象。核心是"中介者职责单一"与"交互简单则不用"。与"对象多 vs 中介者胖"的权衡,避免过度设计。

#
★★

15. Memento 的快照保存与序列化的工程取舍

Memento 模式保存快照与序列化的工程取舍是什么?

  • Memento 模式
  • 快照保存
  • 序列化取舍

Memento(备忘录)在不暴露内部结构的前提下,捕获对象状态并保存,供后续恢复(如撤销操作)。工程取舍:快照的存储——内存中保存快照对象(快但占用内存,适合小状态、短历史),或用序列化持久化到磁盘/DB(适合大状态、长历史、跨会话)。深度 vs 浅拷贝:快照需深拷贝保证状态隔离,否则引用共享导致快照失效。性能与内存:快照数量多时消耗内存,需控制历史深度或压缩。序列化取舍:序列化便于持久化与传输,但引入版本兼容、性能开销。工程上:小型状态用内存 Memento,大型/长历史用序列化 + 版本控制,结合"记录哪些字段"(窄接口)避免过度保存。

Memento 的取舍是"快照的成本与保存方式"。内存快照快但耗内存,序列化持久但慢。核心是深拷贝 + 状态隔离 + 控制历史深度。窄接口(只暴露需要的字段)避免保存整个对象。工程上结合撤销需求与资源预算选择。

#
★★

16. Observer 的"广播 + 注册表"与 EventBus 的关系

Observer 模式的"广播 + 注册表"与 EventBus 有何关系?

  • Observer 模式
  • 广播与注册表
  • EventBus

Observer(观察者)定义"一对多"依赖:主题(Subject)维护订阅者注册表(观察者列表),状态变化时向所有订阅者广播,观察者自动更新。核心是"注册表 + 广播":观察者注册到主题,主题变动后广播通知。EventBus(事件总线)是 Observer 的分布式/解耦扩展:用统一的"总线"解耦发布者与订阅者,发布者发事件到总线,总线分发给订阅者。关系:EventBus 是 Observer 的通用化实现——更彻底解耦(发布者不直接知道订阅者)、支持事件类型过滤、异步分发。Observer 适合"对象直接关联"(主题持有观察者),EventBus 适合"解耦、跨模块"(通过总线中转)。两者都是"发布-订阅"思想的体现。

Observer 的"注册表 + 广播"是发布-订阅的基础,EventBus 是其在"解耦、异步、跨模块"上的工程化。Observer 中主题直接持有观察者,EventBus 通过总线中转、更松散。选择:对象少且直接关联用 Observer,跨模块解耦用 EventBus。

#
★★

17. State 的状态机替换 if-else 嵌套

State 模式如何用状态机替换 if-else 嵌套?

  • State 模式
  • 状态机
  • 替换 if-else

State 模式把"状态"及其行为封装成状态对象,上下文组合当前状态对象并委托行为,状态转换时切换状态对象,从而用"多态分派"替换"if-else 按状态判断"的代码。例如订单状态(待支付/已支付/已发货)每个状态一个类,实现同一接口,处理"支付""发货"等动作时各状态类给出不同行为,状态转换通过切换对象实现。相比 if-else 嵌套:状态模式让每个状态的行为内聚到一个类,可读性、可扩展性(新增状态只需加类)、可测试性(每个状态独立测试)都更好,且符合 OCP(新增状态不修改既有代码)。是"状态机"的面向对象实现。

State 模式把"状态机"从"控制流 if-else"变成"对象组合 + 多态"。状态行为内聚、转换清晰、扩展开放。它避免"状态判断"的散落与嵌套,是复杂状态逻辑的优选。注意状态转换表与状态对象的选择。

#
★★

18. Strategy 的算法族替换条件分支

Strategy 模式如何用算法族替换条件分支?

  • Strategy 模式
  • 算法族
  • 替换条件分支

Strategy(策略)把"一族算法"封装成各自独立的策略对象,都实现同一策略接口,上下文持有策略并在运行时选择调用,从而替换"用 if-else/switch 选择算法"的条件分支。例如价格计算:多种折扣算法(普通/会员/促销)各封装一个策略类,上下文注入策略对象,按需选择。相比条件分支:策略模式把每个算法独立成类,内聚、可测试、可替换、符合 OCP(新增算法只需加策略类,不改条件分支)。运行时可通过注入切换策略。它与 State 相似但目的不同:Strategy 是"算法族可替换",State 是"状态行为随状态变化"。Strategy 是"组合优于继承"的典型应用。

Strategy 用"对象组合 + 接口分派"替代"条件分支选择算法"。算法内聚、可复用、可运行时切换、易扩展。它让"算法"成为可注入的依赖,符合 OCP 与 DIP。判断是否用 Strategy:是否有多个可替换的算法/行为分支。

#
★★

19. Template Method 的骨架共享与可变步骤

Template Method 模式如何共享骨架并支持可变步骤?

  • Template Method 模式
  • 骨架共享
  • 可变步骤

Template Method(模板方法)在基类中定义算法的"骨架"(固定步骤的顺序与流程),把可变的步骤声明为抽象方法或钩子方法(hook),由子类实现这些步骤,从而"共享骨架、定制可变步骤"。例如流程:基类定义 process() 依次调用 validate()、doWork()、cleanup(),其中 doWork() 抽象由子类实现,validate()/cleanup() 可覆写。这样骨架(流程)只写一次(DRY),子类只提供可变步骤,避免重复实现流程。可变步骤(抽象方法/钩子)是扩展点,子类覆写实现定制。它是"继承复用骨架"的经典,骨架稳定时更优,步骤多变时考虑策略。

Template Method 是"骨架固定 + 步骤可变"的继承复用。基类固化流程,子类定制步骤。它避免流程重复(DRY),提供清晰扩展点(hook)。但依赖继承,骨架若非稳定则易脆弱,可与策略组合权衡。

#
★★

20. 现代语言特性对经典模式的替代中 Java record/sealed/interface default、Kotlin data class、函数式 Option/Result 如何替代 Builder、Strategy、Null Object 等模式,替代边界在哪?

现代语言特性如何替代经典模式?Java record/sealed/interface default、Kotlin data class、函数式 Option/Result 替代 Builder、Strategy、Null Object 的边界在哪?

  • 现代语言特性
  • 替代经典模式
  • 替代边界

现代语言特性简化/替代了部分经典模式:Java record 替代 Builder(不可变简单值对象,record 自动生成 equals/hashCode/toString,无需手工 Builder);Java sealed + interface default 组合提供更可控的多态与混入(替代部分继承/开放接口);Kotlin data class 替代 Builder/POJO(提供 copy、equals,构造简洁);函数式 Option/Result 替代 Null Object(用 Some/None、Ok/Err 显式表达可选与错误,替代静默 null 或空对象)。替代边界:record/data class 适合"不可变值对象",若对象有复杂构建逻辑、可变状态,仍用 Builder/类;sealed 可穷尽匹配但不能替代所有策略;Option/Result 替代的是"可空/错误"语义,复杂业务抽象仍需模式。原则:特性天然表达的用特性,特性不覆盖的才用模式。

现代语言特性是"语言级模式"——用语言能力替代样板模式。record/data class 替代值对象与 Builder,sealed 强化多态,Option/Result 替代 null。但边界是"特性覆盖表达能力":复杂构建、可变状态、复杂抽象仍需经典模式。趋势是"语言内建优先于模式样板"。

#

21. Proxy 的 virtual、remote、protective、smart 四大类的工程取舍

Proxy 模式的 virtual、remote、protective、smart 四大类有哪些工程取舍?

  • Proxy 四大类
  • 各类型的用途
  • 工程取舍

Proxy(代理)提供对象的替身,控制对真实对象的访问,四大类:virtual proxy(虚拟代理)——延迟创建昂贵的真实对象,直到真正需要(如懒加载大图);remote proxy(远程代理)——代表远程对象,本地代理转发远程调用(如 RPC、CORBA);protective proxy(保护代理)——控制访问权限,按调用方权限拦截(如鉴权);smart proxy(智能代理)——在访问时附加额外操作(如引用计数、日志、缓存)。工程取舍:virtual 省内存/启动时间,但引入延迟;remote 屏蔽网络细节,但需处理网络错误;protective 加安全层,但需维护权限;smart 增强功能,但增加开销。取舍按"代理收益 vs 开销与复杂度":只有代理的收益(延迟、安全、透明)值得时引入,否则直接访问真实对象。

Proxy 以"代理替身"控制对真实对象的访问,四类各有侧重:virtual 省资源、remote 屏蔽网络、protective 控制权限、smart 附加操作。取舍核心是"代理收益 vs 开销与复杂度",当收益(延迟、安全、透明)值得时才引入代理。

#

22. Visitor 的双分派与 operations on structures

Visitor 模式如何实现"双分派"(double dispatch)?在"对结构进行操作"(operations on structures)方面如何应用?

  • Visitor 模式
  • 双分派
  • 对结构的操作

Visitor(访问者)把"对数据结构的操作"从结构本身中分离出来:结构中的元素(Element)提供一个 accept(visitor) 方法,把"访问者"自身传入,元素再调用 visitor 的 visitXxx(this) 方法,从而让"元素类型"与"访问者方法"在运行时共同决定调用哪个方法,实现双分派(double dispatch)。传统单分派只按对象类型决定方法,而 Visitor 在遍历元素时,由"元素实际类型 + 访问者实际类型"共同决定执行哪个 visit 方法,因此无需对元素做 instanceof 判断。它对"结构"的操作(operations on structures)很实用:同一套 AST、数据结构,可定义多个 Visitor(如求值、打印、类型检查、代码生成、统计),各自在结构上实现一种操作,新增操作只需新增 Visitor 类,而无需修改元素类,符合 OCP。

Visitor 的核心是"双分派"——通过 accept 回调把控制权交回访问者,使元素类型与访问者类型共同决定行为。它把"结构"与"操作"解耦,特别适合 AST/复合结构上需要多种操作、且结构相对稳定的场景。代价是新增元素类型要改所有 Visitor(违反 OCP 的另一面),因此结构稳定而操作多变时用 Visitor 最合适。