Software Architecture / AI Coding / Design Patterns
AI Coding 时代, 架构能力才是程序员的核心竞争力
如果说过去的软件开发难点在“把功能写出来”,那么今天更难的部分已经变成“让快速生成的大量代码仍然可演化、可协作、可验证”。 设计模式的价值,不再只是面试题或教科书知识,而是帮助人类在 AI 时代重新掌控结构、边界和变化。
一、为什么 AI Coding 时代反而更需要设计模式
很多人会误以为,既然 AI 已经可以大段生成代码,设计模式的重要性应该下降。现实恰好相反。AI 让“实现能力”变得廉价,结构失误却因此被成倍放大。 过去一个开发者亲手写几十个分支时,至少会在编码过程中逐渐感受到系统正在失控;今天模型可以在几十秒里生成上百行看似合理的代码,如果没有模式思维约束,系统会更快进入不可维护状态。
设计模式本质上不是“优雅类图”,而是工程世界对重复变化结构的压缩表达。它们关心的不是语言技巧,而是下面这些更根本的问题:哪里在变,谁该知道变化,变化该如何被隔离,系统该如何测试,以及多人协作时影响面如何被控制。
| AI Coding 中常见问题 | 典型表现 | 背后缺失的结构判断 | 模式能解决什么 |
|---|---|---|---|
| 功能越加越乱 | 每来一个需求就多一个分支 | 没有识别出算法或流程中的变化点 | Strategy、Template Method |
| 依赖横向扩散 | 到处直接创建具体对象 | 创建与使用没有分离 | Factory Method、Abstract Factory |
| 横切逻辑失控 | 日志、缓存、鉴权散在各处 | 没有把增强或控制逻辑收束到边界层 | Decorator、Proxy、Facade |
二、理解设计模式,首先要理解“变化点”
大多数教材会从定义开始讲模式,但工程上更有效的顺序是反过来:先看变化点,再看结构。你真的理解一个设计模式,不是因为你记住了它的标准定义,而是因为你能回答五个问题。
- 这个模式到底在隔离哪一种变化?是算法、创建过程、通知链路,还是访问控制?
- 它额外引入了什么结构成本?接口、对象族、代理层还是继承层级?
- 它和相邻模式的边界在哪里?例如 Decorator 与 Proxy,Factory Method 与 Abstract Factory。
- 它在现代框架里通常如何出现?是显式类层级,还是配置、容器、回调、装饰器语法糖。
- 如果不用它,系统通常会退化成什么坏味道?条件泥潭、重复编排、全局状态污染,还是耦合扩散。
三、GoF 23 种设计模式全景图:先看变化,再看结构
GoF 原书把经典设计模式分成创建型 5 个、结构型 7 个、行为型 11 个。[1] 真正值得记住的不是“23 这个数字”,而是每个模式分别把哪一种变化从主流程里拿走:是创建决策、接口不兼容、访问控制、状态切换,还是事件扩散。 如果只背定义,很快会忘;如果抓住“模式隔离的变化点”,大多数模式都能在脑子里自动归位。
| 分类 | 模式 | 第一性问题 | 核心结构动作 | 不用时常见退化 |
|---|---|---|---|---|
| 创建型 | Abstract Factory | 如何切换一整套兼容对象族 | 把对象族创建集中到统一工厂接口 | 风格错配、供应商混搭、装配分散 |
| 创建型 | Builder | 复杂对象如何分步骤构建 | 把构建过程与最终表示解耦 | 长参数列表、构造顺序混乱 |
| 创建型 | Factory Method | 由谁决定具体创建哪一类产品 | 把实例化决策延迟到子类或工厂实现 | 调用方直接依赖具体类 |
| 创建型 | Prototype | 复制是否比重新构造更合适 | 用原型对象派生新实例 | 初始化昂贵、模板对象重复拼装 |
| 创建型 | Singleton | 哪些资源语义上必须全局唯一 | 控制实例数与访问入口 | 全局状态污染、测试相互干扰 |
| 结构型 | Adapter | 旧接口与新接口如何接上 | 做接口翻译,不动既有实现 | 大面积改旧系统、边界层破碎 |
| 结构型 | Bridge | 两个维度同时变化如何避免类爆炸 | 把抽象维与实现维拆开后再组合 | 继承层级按笛卡尔积膨胀 |
| 结构型 | Composite | 部分与整体如何统一对待 | 为叶子与容器定义共同接口 | 遍历和聚合逻辑充满类型分支 |
| 结构型 | Decorator | 如何按需叠加职责 | 用包装对象逐层增强行为 | 组合子类泛滥、横切逻辑扩散 |
| 结构型 | Facade | 复杂子系统如何只暴露一个清晰入口 | 对外收敛为高层接口 | 调用方重复编排底层协作流程 |
| 结构型 | Flyweight | 海量细粒度对象如何共享状态 | 拆分内部共享状态与外部变化状态 | 内存占用随对象数线性暴涨 |
| 结构型 | Proxy | 接口不变时如何改变访问语义 | 在真实对象前加一层控制代理 | 权限、远程性、懒加载散落各处 |
| 行为型 | Chain of Responsibility | 由谁处理请求应否写死 | 把请求沿处理链逐步传递 | 大量 if-else 或巨型分发器 |
| 行为型 | Command | 请求本身是否需要被记录、排队、撤销 | 把动作对象化 | 调用难审计、难重放、难撤销 |
| 行为型 | Interpreter | 业务规则能否抽成一门小语言 | 把语法规则和求值过程对象化 | 规则分支散在代码里无法组合 |
| 行为型 | Iterator | 如何遍历集合而不暴露内部表示 | 为遍历定义统一协议 | 每种容器一套遍历 API |
| 行为型 | Mediator | 对象之间的网状耦合如何收束 | 把协作规则集中到中介者 | 对象互相认识、关系图失控 |
| 行为型 | Memento | 如何在不破坏封装下保存与恢复状态 | 把快照封装成外部可持有对象 | 撤销点粗糙、状态恢复不完整 |
| 行为型 | Observer | 一个事实发生后如何通知多方响应 | 发布与订阅解耦 | 主流程不断追加“顺手做”的动作 |
| 行为型 | State | 状态变化时行为为何总在分支里爆炸 | 把状态相关行为移到状态对象 | 状态机写成巨型条件语句 |
| 行为型 | Strategy | 同一目标下多算法如何替换 | 把算法族抽到统一接口后注入 | 条件泥潭、算法散落、难扩展 |
| 行为型 | Template Method | 稳定流程骨架与可变步骤如何共存 | 父类固定顺序、子类实现局部 | 流程复制、步骤顺序失控 |
| 行为型 | Visitor | 结构稳定时如何持续增加新操作 | 把操作从对象结构中抽离 | 每加一个操作都要改一遍节点类 |
创建型模式
你真正要问的不是“怎么 new”,而是“由谁决定创建什么、创建过程是否稳定、创建结果是否需要成套一致”。
Abstract Factory / Builder / Factory Method / Prototype / Singleton
结构型模式
核心不是类图好不好看,而是对象彼此如何连接,才能在接口兼容、访问控制、层次组织和横切增强上不失控。
Adapter / Bridge / Composite / Decorator / Facade / Flyweight / Proxy
行为型模式
关注的是职责如何流动、算法如何替换、状态如何切换、消息如何传播,以及一段行为是否应该被对象化。
Chain of Responsibility / Command / Interpreter / Iterator / Mediator / Memento / Observer / State / Strategy / Template Method / Visitor
第一性原则
模式不是为了减少代码行数,而是为了把变化点封装进最小认知边界,让第五次迭代也不至于拖垮系统。
AI Coding 时代更要反着学:先识别变化和退化路径,再决定模式,而不是先背术语再找题目去套。
Abstract Factory:当你要切换的不是一个对象,而是一整套彼此兼容的系统
关键词:对象族、一致性、供应商切换、成套装配。
Abstract Factory 的第一性问题是“一致性本身是不是需求”。如果调用方切换的不是单个按钮,而是按钮、对话框、输入框、主题色、图标规范这一整套配套对象,那么你要管理的就不是创建一个类,而是创建一整个兼容对象族。 这时最可怕的不是“多写几个构造函数”,而是局部看都对、整体风格却混乱。
它和 Factory Method 的边界非常清晰:Factory Method 解决“这一个对象到底创建谁”,Abstract Factory 解决“这一组对象必须同时切到哪一套家族”。前者偏单点延迟决策,后者偏整体一致性。前端主题系统、数据库方言层、多云厂商封装都是它的高频落点。
- 变化点:产品家族整体切换,而不是单个产品替换。
- 适合:主题系统、跨平台 UI、数据库方言、多供应商 SDK。
- 误用风险:如果只有一个产品在变,用 Abstract Factory 只会平添抽象层。
- 边界辨析:它解决的是“家族一致性”,不是“复杂构建步骤”。
Builder:当对象复杂到“怎么组装”本身已经成为问题
关键词:分步骤构造、构建顺序、最终表示、参数风暴治理。
Builder 的核心不是 fluent API 好不好看,而是“构建过程能不能独立管理”。如果一个对象需要分阶段校验、按顺序填充、允许可选步骤组合,或者同一组构建步骤能产出不同表示,说明复杂度已经从“对象是什么”转移到了“对象怎么被拼出来”。
这时,把一长串参数都塞进构造器只是把复杂度藏起来。Builder 则显式承认构建过程本身就是变化点。它非常适合请求对象、复杂查询、报表配置、工作流定义和测试夹具搭建。
- 变化点:构建顺序、可选步骤、最终表示。
- 适合:复杂配置对象、SQL/DSL 组装、报表定义、测试数据构造。
- 误用风险:对象并不复杂时,Builder 会把简单构造人为拉长。
- 边界辨析:它关注“步骤化构建”,不是“成套对象族切换”。
Factory Method:让调用方不必知道“具体类怎么被选出来”
关键词:延迟创建、具体类解耦、扩展入口收束。
Factory Method 的第一性问题是“创建决策应不应该落在使用方手里”。当调用方只需要一个导出器、解析器、客户端、存储后端时,它不该同时承担环境探测、配置读取、生命周期初始化和具体实现选择。
在框架里,这个模式几乎无处不在,因为框架天然需要把“创建时机”和“创建谁”留成扩展点。Flask 的 Application Factory、容器中的 bean 创建思想,都体现了把实例化决策从业务调用方移走的价值。[9][4]
- 变化点:具体产品类型、装配时机、创建分支。
- 适合:报表、解析器、存储客户端、消息发送器。
- 误用风险:如果只有一个稳定实现,为工厂而工厂只会制造噪音。
- 边界辨析:它解决“单对象创建决策”,不是“复杂对象拼装”。
Prototype:当复制一个成熟对象,比重新构造更便宜也更稳定
关键词:样板对象、复制派生、初始化成本、模板变体。
Prototype 不是“会拷贝对象”这么简单,它真正解决的是“重新构造的成本是不是过高”。如果某个对象初始化昂贵、默认组合很多、局部变体频繁,那么从一个已经完成大部分准备工作的原型派生,通常比每次从零 new 再挨个设置字段更可靠。
这个模式在图形编辑器、规则配置、游戏对象、复杂模板生成里尤其自然。它把“共同部分”沉到原型对象里,再把“差异部分”留给复制后的少量改动。现代语言中如果复制能力已经很好,Prototype 的存在感会减弱,但其设计意图仍然成立。
- 变化点:初始化代价和模板变体,而不是产品接口。
- 适合:图元复制、默认配置派生、规则模板、表单模版。
- 误用风险:浅拷贝/深拷贝边界不清时,容易复制出共享状态 bug。
- 边界辨析:它解决“从现成对象派生”,不是“分步骤创建”。
Singleton:只有在“语义上必须唯一”时才成立,而不是因为传依赖麻烦
关键词:全局唯一、生命周期控制、状态污染风险。
Singleton 最常被误学成“一个对象只能有一个实例”,但更准确的理解是“有极少数资源在语义上必须以全局唯一身份出现”。例如进程级配置源、注册表、底层资源门面。这是业务约束,不是偷懒理由。
今天更稳妥的工程做法通常是让容器管理 singleton scope,而不是每个类都手写单例。因为一旦实例里带可变状态,测试污染、并发时序、生命周期残留会迅速放大。GoF 作者后来也反复提醒过 Singleton 非常容易沦为坏味道。[2]
- 变化点:实例数和生命周期,而不是业务算法。
- 适合:配置源、注册表、不可重复初始化的底层门面。
- 误用风险:把依赖注入问题伪装成“全局唯一”。
- 边界辨析:业务对象默认不该因为方便访问就被做成单例。[4]
Adapter:新系统想要干净接口,但现实世界通常是一堆旧接口
关键词:接口翻译、边界兼容、遗留系统缓冲层。
Adapter 的第一性问题不是“代码风格统一”,而是“旧系统能不能不动”。真正的工程环境里,你经常面对遗留系统、第三方 SDK、老协议和新服务。它们不是坏,只是接口形状与你现在想要的抽象不一致。
Adapter 的价值就在于把“不一致”封在边界层,让系统内部继续沿着更干净的接口发展。Requests 的 Transport Adapter 和 Spring MVC 的 HandlerAdapter 都是工业化示例。[10][6]
- 变化点:接口形状与参数语义,而不是业务职责。
- 适合:遗留接入、第三方网关统一封装、协议迁移期兼容。
- 误用风险:如果新旧接口差异其实是业务概念差异,单纯翻译会掩盖模型问题。
- 边界辨析:Adapter 是翻译,Decorator 是增强,Proxy 是控权。
Bridge:当变化轴不止一条时,先拆轴,再组合
关键词:抽象维、实现维、正交变化、类爆炸治理。
Bridge 专门处理“两个维度都在变”的情况。比如消息类型会变,发送渠道也会变;图形形状会变,渲染后端也会变。如果直接用继承,把两个维度绑在一棵树上,类数量会按笛卡尔积膨胀。
Bridge 的核心动作是:把抽象维和实现维拆开,让它们分别演化,再通过组合连接。这样新增一个渠道不需要复制全部消息类型,新增一个抽象也不需要重新派生所有实现类。
- 变化点:两个长期独立变化的维度。
- 适合:多渠道通知、多平台渲染、多驱动设备控制。
- 误用风险:如果只有一个维度在变,Bridge 会比普通组合更重。
- 边界辨析:它和 Strategy 都用组合,但 Bridge 管“两个轴”,Strategy 管“一个算法位”。
Composite:当树形结构中的“单个”和“整体”都必须被统一处理
关键词:部分-整体、递归组合、统一接口、树结构。
Composite 的第一性问题是“调用方是否必须反复区分当前面对的是叶子还是容器”。如果一个系统天然是树形的,如文件与文件夹、图元与图层、组件与子组件,那么每次都在外面判断类型,说明结构抽象还不够稳定。
Composite 通过共同接口把“局部”和“整体”统一起来。你得到的真正好处,不是类图看起来工整,而是渲染、聚合、遍历、批处理都能用递归自然展开,认知成本显著下降。
- 变化点:节点层次和递归聚合行为。
- 适合:文件系统、UI 组件树、组织架构树、表达式树。
- 误用风险:叶子与容器能力差异过大时,硬统一接口会制造空方法。
- 边界辨析:Composite 统一结构,Visitor 统一的是“对这棵结构做什么”。
Decorator:当能力需要按需叠加,而不是预先长成一棵继承树
关键词:职责增强、横切逻辑、按需叠加、组合优先。
Decorator 真正解决的是“增强能力是否可以被拆成独立层并自由组合”。如果你需要给对象加日志、缓存、重试、限流、鉴权、压缩、埋点,继承会很快变成组合子类爆炸,而一层层包装则能把每种增强职责拆干净。
Python 里的装饰器语法就是这种思想大规模普及后的语法糖。重要的不是有没有 @decorator,而是增强逻辑是否能独立存在、是否能调整顺序、是否能在不改原对象的前提下撤掉。
- 变化点:横切增强能力及其叠加顺序。
- 适合:日志、缓存、重试、压缩、指标采集。
- 误用风险:包装层过多会让调用链排障困难。
- 边界辨析:它增强职责,但一般不改变“是否允许访问”。
Facade:把复杂性留在系统内部,而不是散落到每一个调用点
关键词:统一入口、编排收束、认知减负。
Facade 不是让复杂系统“变简单”,而是让复杂性待在该待的地方。下单、扣款、库存预占、通知、物流、审计这类协作经常一起出现。如果每个调用方都各自拼一遍流程,系统会快速出现重复编排和分叉逻辑。
门面的作用是把稳定协作流程集中收口,对外只暴露一个高层接口。这样你就能在一个地方统一处理异常、回滚、日志和顺序约束,而不是靠调用方自己“记得怎么调”。这是 AI 生成代码时代极其重要的结构闸门。
- 变化点:子系统协作流程和对外入口数量。
- 适合:下单、导出、聚合搜索、转码、支付编排。
- 误用风险:Facade 很容易膨胀成无所不包的上帝对象。
- 边界辨析:Facade 简化对外入口,Mediator 集中的是内部对象协作规则。
Flyweight:只有当对象数量和内存占用真的成为问题时,它才有意义
关键词:共享内部状态、海量对象、内存优化、外部化变化。
Flyweight 不主要解决业务抽象问题,而是解决资源成本问题。它假设大量细粒度对象之间存在可共享的不变部分,于是把这部分抽出来共享,把随上下文变化的那部分留给外部传入。
文本编辑器中的字形信息、地图里的图标元数据、棋盘格样式、粒子系统渲染参数都能体现这个思路。没有对象量级和性能压力时不要上它,因为它会把简单对象模型变得更难读、更难调试。
- 变化点:内部不变状态与外部变化状态的切分方式。
- 适合:字符字形、图标、粒子、规则元数据等高复用对象。
- 误用风险:为了炫技过早做共享,结果把业务模型拆得支离破碎。
- 边界辨析:它的主要收益是内存与对象数,而不是语义清晰度。
Proxy:接口看起来一样,但访问语义其实已经变了
关键词:访问控制、懒加载、远程性、权限边界。
Proxy 和 Decorator 的类图常常很像,但解决的问题完全不同。Proxy 关注的是“你能不能直接碰真实对象”,或者“真实对象是否应该现在就被实例化”。权限校验、远程调用、事务边界、缓存命中、懒加载,都是访问语义问题。
Spring AOP 的大量能力都建立在代理机制上;ORM 中的懒加载对象也经常是代理思想的实现。它让调用方面对一个稳定接口,但在真实访问前多了一层决策与控制。[6]
- 变化点:访问前后需要附着的控制语义。
- 适合:权限、远程服务、事务边界、懒加载、保护性包装。
- 误用风险:把普通增强都做成代理,会让职责意图变模糊。
- 边界辨析:Proxy 关注“能不能、何时、以何种方式访问”,不是“增加新职责”。
Chain of Responsibility:让“谁来处理这个请求”成为可配置的链,而不是写死的分支
关键词:请求链、逐步处理、短路、可插拔责任节点。
Chain of Responsibility 的核心是把“处理权”从调用点剥离出来。一个请求沿着责任链流动,每个节点都可以处理、修改、放行或终止。这样发送者不必认识最终处理者,也不必知道处理顺序的全部细节。
审批链、过滤器链、中间件链、风控链、告警处理链都非常适合它。真正的收益不是“看起来很优雅”,而是新增一个责任节点时,旧节点通常不用被改动。
- 变化点:处理顺序、处理权归属、拦截点数量。
- 适合:审批流、过滤器、中间件、规则链、插件式处理。
- 误用风险:链太长且有副作用时,问题定位会很痛苦。
- 边界辨析:它是“请求沿链流动”,不是“事件广播给所有人”。
Command:当动作本身需要被排队、记录、撤销或重放时,请把它对象化
关键词:请求对象化、撤销重做、任务排队、审计重放。
Command 的第一性问题是“这个动作是不是也要像数据一样被保存和传递”。如果一次操作需要进队列、做审计、支持撤销重做、组合成宏命令、跨线程或跨进程转交,那么普通函数调用就不够了,因为调用一旦发生就只剩结果。
把请求对象化后,调用者、接收者和执行时机就被彻底拆开。编辑器中的撤销重做、任务系统中的作业描述、工作流动作单元都体现了这种设计思路。
- 变化点:执行时机、执行记录、撤销能力、目标接收者。
- 适合:任务队列、撤销重做、批处理、宏命令、审计系统。
- 误用风险:不会被保存或调度的简单调用,用 Command 只是加壳。
- 边界辨析:Command 是“请求对象化”,Strategy 是“算法对象化”。
Interpreter:当业务规则开始像一门小语言时,才考虑为它建立语法与求值器
关键词:小型 DSL、语法规则、表达式求值、规则组合。
Interpreter 适合的不是“任意复杂规则”,而是规则规模已经超出简单配置和条件分支、但又还没大到需要完整编译器框架的那段区间。它把语法规则和求值逻辑都对象化,让规则能够被组合、嵌套和解释执行。
告警表达式、过滤条件、权限判断式、促销规则、小型查询语法都可能进入这个区域。它的价值在于把规则从代码 if-else 里抽出来,但代价是语法设计、错误提示和性能问题都会随之出现。
- 变化点:语法组合方式与求值规则,而非单条规则内容。
- 适合:小 DSL、规则表达式、查询语言、过滤器语法。
- 误用风险:语法一复杂就会迅速走向“自己造半个编译器”的深坑。
- 边界辨析:Interpreter 解决“规则像语言”,不是“规则很多”。
Iterator:把“如何遍历”从“集合内部怎么存”里分离出来
关键词:统一遍历协议、容器解耦、顺序访问、惰性消费。
Iterator 今天显得不那么“模式化”,是因为很多语言已经把它内建成标准协议。可它解决的问题并没有消失:调用方需要顺序访问元素,但不应该依赖集合的内部表示。否则每种容器都会带来一套不同的遍历 API。
Java 的 Iterator 接口和 Python 的迭代器协议,正是这个模式被语言与标准库吸收后的结果。学它的重点不是手写 next(),而是理解统一遍历协议为什么能大幅降低容器抽象的成本。[14][15]
- 变化点:集合内部组织方式与遍历策略。
- 适合:自定义容器、惰性序列、树遍历、批量消费管道。
- 误用风险:为本来就能用标准迭代协议的场景再造一层自定义迭代器。
- 边界辨析:Iterator 管“如何访问元素”,Composite 管“元素怎样组成树”。
Mediator:先砍掉对象之间的网状连线,再把协作规则集中到一个地方
关键词:协作中枢、网状耦合治理、交互收束。
Mediator 适合那种“每个对象都认识其他很多对象”的系统。表单联动、复杂 UI、聊天室、工作流节点协同,只要对象间交互一多,局部改动就会牵一发动全身。此时问题不是单个对象职责不清,而是协作规则散落在太多对象里。
通过引入中介者,你把原来 N 对 N 的互相引用变成每个参与者只依赖 mediator。这样协作规则被集中表达,参与者角色也更清晰。但如果所有逻辑都堆进 mediator,它又会反过来变成巨大的调度中心。
- 变化点:对象协作规则与交互关系图。
- 适合:复杂表单、聊天室、工作流编排节点、控制中心。
- 误用风险:中介者变成“所有东西都来问我”的超级类。
- 边界辨析:Mediator 集中内部协作规则,Facade 简化对外使用入口。
Memento:保存状态不是难点,难点是怎样在不破坏封装的前提下恢复它
关键词:快照、撤销点、恢复、封装保护。
Memento 的第一性问题是“状态恢复应否暴露内部细节”。如果一个对象需要支持撤销、回滚或快照,你当然可以把内部字段全抛出来给外部记,但那会直接破坏封装。Memento 让状态保存成为一个被允许的出口,却不要求外界理解对象内部结构。
编辑器文档、画布操作、局部会话状态、表单临时草稿都很常见。真正的难点在于明确哪些状态属于快照的一部分,哪些是派生值、外部资源或副作用。否则“恢复”只会恢复出一个看似相似、实际不一致的对象。
- 变化点:可回滚状态边界和快照粒度。
- 适合:撤销重做、草稿恢复、局部快照、时间点回看。
- 误用风险:把外部副作用也想当然地纳入回滚,会产生虚假的一致性幻觉。
- 边界辨析:Memento 保存的是状态,Command 保存的是动作。
Observer:事实发布出去,但发布方不该知道所有反应方是谁
关键词:发布订阅、事件传播、附加行为解耦。
Observer 最重要的思想是把“事情发生了”与“谁要响应”分开。订单支付成功后,积分、通知、审计、推荐更新、风控记录都可能要行动。如果主流程亲自调所有下游,每增加一个消费者都得回头修改主链路。
Spring 的 ApplicationEventPublisher 和 Django Signals 都是这种思想的经典工程化体现。它们不是为了炫耀解耦,而是让主流程只负责发布事实,把附加行为开放给订阅者自己接住。[5][8]
- 变化点:响应者集合、订阅关系、扩展动作数量。
- 适合:领域事件、通知系统、插件机制、扩展钩子。
- 误用风险:把事件机制当成异步和一致性的万能药。
- 边界辨析:Observer 是“一处变化多方响应”,Chain 是“一个请求依次流经多环”。
State:行为不是随机变化,而是被内部状态机驱动着变化
关键词:状态机、状态迁移、行为切换、条件爆炸治理。
State 模式真正要解决的是“状态分支已经开始侵蚀核心对象”。当一个对象在待支付、已支付、已发货、已取消等不同状态下允许的操作和行为都不同,如果你把这些逻辑都写在一个类里,最终一定会演变成巨大的条件判断矩阵。
State 把状态特有行为移到独立状态对象中,让上下文对象只负责持有当前状态和触发迁移。这样状态迁移规则、非法操作和状态特有副作用都能变得显式,也更容易测试。
- 变化点:行为随内部状态变化而切换。
- 适合:订单状态机、连接状态、工作流节点、游戏角色状态。
- 误用风险:如果状态很少且基本不扩展,State 可能比普通分支更重。
- 边界辨析:State 是“对象因内部状态不同而变”,Strategy 是“外部选择不同算法”。
Strategy:把变化算法从主流程里拔出来,而不是继续给主流程加分支
关键词:算法家族、可替换、运行时注入、条件泥潭治理。
Strategy 的本质,是承认“算法”本身就是变化点。折扣规则、路由策略、排序规则、推荐召回、风控评分,这些场景的目标一致,但实现规则常常变化。如果主流程中不断累加 if / elif / else,最终会形成又长又脆的条件泥潭。
AI Coding 场景里,这一点尤其重要。模型对“多加一个分支”极其顺手,但不会天然识别这些分支其实属于一组可替换算法。能否把这类变化从主流程里抽成策略接口,往往决定系统在第三、第四次迭代后是否还能继续演化。
- 变化点:同一职责下的多种算法。
- 适合:定价、排序、调度、风控、压缩、推荐、审核。
- 误用风险:只有一个稳定实现时,抽策略只会增加跳转层。
- 边界辨析:Strategy 变的是算法,Template Method 变的是骨架中的局部步骤。
Template Method:主流程骨架先固定,再把真正变化的步骤让出去
关键词:算法骨架、步骤延迟、流程稳定、继承钩子。
Template Method 适合那种“流程顺序稳定,但局部步骤可变”的问题。比如 ETL、数据库访问、导出流程、资源获取与释放。你不希望每个调用方都自己拼接顺序,也不希望把所有细节都写死在同一个实现里。
它通过父类固定流程骨架,再让子类实现个别步骤,从而确保关键顺序不被破坏。Spring 的 JdbcTemplate 就是经典工业案例:资源管理和主流程由模板兜住,调用方只提供真正变化的行为。[7]
- 变化点:固定骨架中的局部步骤。
- 适合:ETL、数据库模板、固定审批流程、统一资源管道。
- 误用风险:继承层次过深时,很容易进入脆弱基类与继承地狱。
- 边界辨析:Template Method 靠继承固化骨架,Strategy 靠组合替换算法。
Visitor:结构稳定时,把新操作持续从结构外侧长出来
关键词:操作外提、稳定结构、多操作扩展、双分派。
Visitor 的第一性问题是“到底是数据结构更稳定,还是操作集合更稳定”。如果节点结构相对稳定,但你要不断新增分析、导出、校验、统计、格式化等操作,那么把每个新操作都塞回节点类中,会让结构类不断膨胀。
Visitor 的解决方式是把操作从结构中抽走,新操作通过新增 visitor 来接入,而不必改每个节点类。AST、规则树、编译器节点、复杂报表对象图都非常适合这种模式。代价则是新增节点类型时,所有 visitor 都要跟着补。
- 变化点:不断新增的操作集合,而不是节点结构本身。
- 适合:AST、表达式树、规则引擎节点、报表对象图。
- 误用风险:如果结构本身变得很快,Visitor 会让维护成本反向爆炸。
- 边界辨析:Composite 解决“树怎么长”,Visitor 解决“对树做什么新操作”。
四、23 个模式放在一起时,应该怎么选
真正的难点从来不是“记不住名字”,而是“两个模式看起来都像能用,到底该上谁”。下表先给出问题到模式的映射,再给出最容易混淆的一组组边界。判断时优先看变化点,不要先看类图长得像不像。
| 真正的问题 | 优先考虑的模式 | 判断句 |
|---|---|---|
| 同一目标有多种算法 | Strategy | 变化的是算法,不是主流程骨架 |
| 流程顺序稳定,但步骤细节会变 | Template Method | 先固定骨架,再放开局部步骤 |
| 对象创建过程会变化 | Factory Method | 使用方不该知道具体实现如何被实例化 |
| 需要切换一整套兼容组件 | Abstract Factory | 你要切换的是对象家族,不是单个对象 |
| 对象构造非常复杂 | Builder | 构建过程本身已经值得被建模 |
| 复制现成对象比重建更划算 | Prototype | 初始化成本和默认配置比复制更重 |
| 旧接口和新接口不兼容 | Adapter | 先补翻译层,而不是大拆旧系统 |
| 两个维度同时变化 | Bridge | 先拆变化轴,再做组合 |
| 结构是树,部分与整体都要统一操作 | Composite | 调用方不应反复区分叶子和容器 |
| 横切增强需要可叠加 | Decorator | 增强职责,而不是造一堆组合子类 |
| 访问前要做权限、远程、懒加载控制 | Proxy | 接口不变,但访问语义改变 |
| 子系统太复杂,对外要统一入口 | Facade | 调用方不该承担底层编排 |
| 对象数量极大,内存成为主要问题 | Flyweight | 共享内部状态,外部传入变化部分 |
| 一个事件要通知多个响应者 | Observer | 发布方不应认识所有消费者 |
| 请求该由谁处理需要动态决定 | Chain of Responsibility | 让请求沿责任链流动 |
| 动作需要排队、审计、撤销、重放 | Command | 请求本身也要像数据一样被管理 |
| 对象行为随状态机切换 | State | 变化的是内部状态,不是外部算法选择 |
| 对象之间交互网状耦合过密 | Mediator | 先收拢协作规则,再谈局部职责 |
| 需要保存并恢复内部状态 | Memento | 恢复不能靠暴露内部字段来完成 |
| 需要为小型 DSL 定义规则与求值 | Interpreter | 规则开始像一门语言而不只是配置 |
| 需要统一遍历不同容器 | Iterator | 遍历协议不该依赖内部存储结构 |
| 结构稳定,但不断要加新操作 | Visitor | 把操作从节点类里抽出去 |
| 确实需要全局唯一实例 | Singleton | 但先证明这真是业务约束,不是偷懒 |
| 容易混淆的组合 | 本质差别 | 一句更准确的判断 |
|---|---|---|
| Factory Method vs Abstract Factory | 一个管单对象创建,一个管对象族一致性 | 只切一个产品时别上对象家族工厂 |
| Builder vs Prototype | 一个强调构建步骤,一个强调从样板复制 | 复杂在“怎么拼”还是复杂在“初始化太贵” |
| Adapter vs Decorator vs Proxy | 翻译接口、增强职责、控制访问 | 先问意图,再看结构 |
| Bridge vs Strategy | 两个正交变化轴 vs 一个算法位可替换 | 类在按笛卡尔积膨胀时先想到 Bridge |
| Strategy vs State | 外部选择算法 vs 内部状态驱动行为 | 有没有状态迁移图,是判断关键 |
| Template Method vs Strategy | 继承固化骨架 vs 组合替换算法 | 顺序是否必须被父类强约束 |
| Facade vs Mediator | 对外简化入口 vs 对内集中协作规则 | 调用者在系统外还是对象群内部 |
| Composite vs Visitor | 统一树结构 vs 给稳定树加新操作 | 变化的是结构还是操作集合 |
| Command vs Memento | 保存动作 vs 保存状态 | 撤销是靠重放反向动作还是恢复快照 |
| Observer vs Chain of Responsibility | 广播给多个响应者 vs 顺序流经处理链 | 一个事实是否应被多人同时感知 |
五、AI Coding 时代最常见的模式误用:不是“不会模式”,而是“太快地乱用模式”
当模型能在几十秒里生成大量代码时,模式误用的代价会被放大。最危险的情况往往不是没有模式,而是模型学会几个术语之后,到处套接口、套工厂、套代理,看起来很工程化,实际只是在制造更多认知层级。
| 误用信号 | 表面上看在做什么 | 实际后果 | 更稳妥的做法 |
|---|---|---|---|
| 只有一个实现,却先造接口和工厂 | “提前留扩展点” | 跳转层增多,定位更慢,扩展并未真实发生 | 先直写实现,等变化出现再抽象 |
| 把所有共享资源都做成 Singleton | “全局方便访问” | 测试污染、并发语义模糊、依赖隐藏 | 优先 DI 容器与显式依赖传递 |
| 所有附加逻辑都发事件 | “彻底解耦” | 顺序不清、一致性松散、观测困难 | 只有真正需要多方响应时再上 Observer |
| Decorator / Proxy / Adapter 混着用 | “反正都是包一层” | 设计意图模糊,排障时根本不知道边界在哪 | 先回答这层是翻译、增强还是控权 |
| 用 Template Method 堆很深继承层级 | “抽出公共流程” | 脆弱基类、子类耦合、修改牵一发而动全身 | 流程不稳定时优先组合与策略注入 |
| 把 Flyweight 用到普通业务对象上 | “提前优化内存” | 对象模型难懂,性能收益却不存在 | 先有对象量级和瓶颈证据再做共享 |
| 把 Interpreter 用在规则稍复杂就上 DSL | “更灵活” | 开始自己维护半门语言,错误提示与调试代价暴涨 | 先看数据驱动配置是否已经足够 |
| 责任链没有日志和顺序约束 | “可插拔” | 链路行为不可见,副作用难定位 | 显式声明顺序、短路语义和追踪信息 |
六、为什么现代框架反复长成这些模式
设计模式不是教科书里孤立的名词,而是主流框架长期演化后自然收敛出的结构。只要你开始理解框架为什么那样设计,就会发现它们不断重现的并不是“语法技巧”,而是相同的变化管理问题。[2][11][12]
| 框架/机制 | 背后的模式 | 真正值得理解的点 |
|---|---|---|
| Spring Bean 创建与作用域 | Factory Method / Abstract Factory / Singleton | 对象创建与生命周期不应该散落在业务代码里。[4] |
| Spring AOP | Proxy | 横切控制和访问语义可以借代理层统一织入。[6] |
| Spring JdbcTemplate | Template Method | 流程骨架与资源管理由模板兜住,变化行为通过回调注入。[7] |
| Django Signals | Observer | 发布事实与附加响应解耦,但一致性仍需单独设计。[8] |
| Flask Application Factory | Factory Method | 创建应用对象本身就是框架扩展点。[9] |
| Requests Transport Adapter | Adapter | 网络传输层差异通过统一适配接口收口。[10] |
| Java Iterator / Python 迭代协议 | Iterator | 经典模式会被语言吸收进标准接口,但设计意图不会消失。[14][15] |
这件事对 AI Coding 的意义非常直接:如果你不理解框架底层依赖的是哪类结构,模型就会为你生成“能跑,但不顺着框架哲学”的代码。短期可能能过,长期通常会在扩展、测试和维护阶段集中还债。
七、最后的判断:模式不是为了背诵,而是为了提前看见代码未来会怎么坏
设计模式在今天真正重要,不是因为它们属于经典,而是因为它们提供了一套跨语言、跨框架、跨时代都稳定有效的结构判断工具。 当 AI 让代码生成变得极快时,人类最值钱的能力就不再是“我能把第一版写出来”,而是“我知道这段代码在第五次迭代后会朝哪里腐化,以及该如何在一开始就把腐化路径切断”。[13][16]
所以,理解 GoF 23 种模式,并不是为了把系统写得更像教科书;而是为了让你在面对不断变化的需求、不断扩张的调用链、不断累积的技术债时,始终还能用一套稳定的语言描述问题、切开问题、验证问题。
参考文献
[1] Gamma, E., Helm, R., Johnson, R., and Vlissides, J. Design Patterns: Elements of Reusable Object-Oriented Software 相关书目概述。
[2] Gamma, E., Helm, R., Johnson, R., and Vlissides, J. Design Patterns: 15 Years Later.
[3] Fowler, M. Inversion of Control Containers and the Dependency Injection Pattern.
[4] Spring Framework Reference. Bean Scopes.
[5] Spring Framework Javadoc. ApplicationEventPublisher.
[6] Spring Framework Reference. Proxying Mechanisms.
[7] Spring Framework Reference. JDBC Core and JdbcTemplate.
[8] Django Documentation. Signals.
[9] Flask Documentation. Application Factories.
[10] Requests Documentation. Transport Adapters.
[11] SourceMaking. Design Patterns Catalog.
[12] Refactoring.Guru. Design Patterns Catalog.
[13] Refactoring.Guru. The History of Design Patterns.
[14] Oracle Java SE 8 API. Iterator.
[15] Python Documentation. Iterator Types.
[16] Ampatzoglou, A., Chatzigeorgiou, A., Charalampidou, S., and Avgeriou, P. The effect of GoF design patterns on stability: A case study. Information and Software Technology, 2015.
[17] Ampatzoglou, A., et al. The state of the art on software engineering design patterns: A systematic mapping study. Journal of Systems and Software, 2016.