12 KiB
12 KiB
| name | description |
|---|---|
| software-design-patterns | Use when code structure is under pressure — growing if/switch branches, scattered status checks, tight coupling to third-party SDKs, unclear layering or transaction boundaries, extensibility demands, domain modeling, or cross-service reliability (dual writes, retries, sagas, idempotency). Guides selecting, rejecting, and combining GoF, enterprise, DDD, and distributed-system patterns by evidence, and recommends keeping the code simple when the pressure is unproven. Trigger on 重构、解耦、架构评审、扩展点、状态机、双写一致性、重试熔断、CQRS requests even if the user never says "设计模式". |
软件设计模式:选择、拒绝与组合
参考文件是设计决策记录,不是名词解释。按"压力 → 候选 → 排除"推进,不要从模式名反推需求。
核心原则
- 先识别变化点:什么在变?变化频率?是否已产生维护成本?
- 最小复杂度优先:简单分支、普通函数、组合、语言原生特性够用时不引入模式。
- 收益必须可验证:降耦合、隔离变化、维护不变量、明确事务边界、提可靠性或可测性。
- 成本必须计入:接口、对象、间接调用、状态、异步、持久化、运维。
- 不为"未来可能"设计:只有变化有证据或结构已产生压力时才加抽象。
工作流
- 先查日志:若项目已有设计决策日志,读索引,按模块/变化轴定位受影响的既有记录(见「设计决策日志」)。需求变更时从这一步开始,不要重新从零判断。
- 说清压力:指出条件分支、耦合、职责、状态、事务、一致性或可靠性上的具体问题。
- 路由:用下方「快速路由」表筛出 最多 3 个候选。若命中「停止条件」,直接建议保持简单,不再读任何参考文件。
- 验证候选:打开候选模式文件,只看
什么时候使用/什么时候不要使用/常见误用/退出条件。 - 组合检查:推荐 2 个及以上模式时,读 常见模式组合,确认职责不重叠、故障链可解释。
- 输出:按「输出模板」给结论。
- 落日志:把结论写入设计决策日志。不是可选项——没有记录的决策等于下次需求变更时重新猜。
只在路由表未命中、问题跨多个层级、或需要评分/迁移顺序时,才读 设计模式决策指南。
快速路由
| 设计压力 | 首选候选 | 通常不该用的情况 |
|---|---|---|
if/switch(type) 随实现数量增长 |
Strategy + Factory Method | 分支少且稳定 |
if/switch(status) 散落 |
State | 状态仅是数据标签 |
| 固定流程中少数步骤变化 | Template Method | 变化维度多、组合需求强 |
| 连续规则/过滤且可短路 | Chain of Responsibility | 步骤强耦合、全部必须执行 |
| 一个事实触发多个反应 | Observer / Domain Event | 强一致动作必须同事务完成 |
| 第三方/遗留接口不兼容 | Adapter | 接口本来就兼容 |
| 横切访问控制、事务、远程替身 | Proxy | 只是动态叠加功能时更像 Decorator |
| 功能可按需层层增强 | Decorator | 包装顺序复杂到难以理解 |
| 两个正交变化维度导致类爆炸 | Bridge | 只有一个变化维度 |
| 创建参数多、构造分阶段 | Builder | 简单构造/命名参数已足够 |
| 需要成套切换产品族 | Abstract Factory | 只有一种产品 |
| 复杂子系统需要统一入口 | Facade | Facade 只是无价值转发 |
| 树形结构统一处理叶子/容器 | Composite | 实际是图或行为差异巨大 |
| 领域对象需要稳定身份 | Entity | 只由值决定时用 Value Object |
| 领域对象只由值决定 | Value Object | 有独立身份/生命周期 |
| 强一致对象组需要事务边界 | Aggregate + Aggregate Root | 只是数据库关联关系 |
| 领域规则不自然属于实体 | Domain Service | 只是应用编排或技术服务 |
| 聚合持久化需要隔离 ORM | DDD Repository | 简单 CRUD 或报表查询 |
| 集合式持久化访问抽象 | Enterprise Repository | 富领域模型应按聚合根建模 |
| DB 更新 + MQ 发布双写 | Outbox | 没有跨进程事件传播 |
| 网络瞬态失败 | Retry + Timeout | 永久错误、非幂等写 |
| 持续故障导致资源耗尽 | Circuit Breaker + Bulkhead | 本地廉价调用 |
| 跨服务长事务 | Saga | 单库 ACID 足够 |
| 写模型复杂、读模型差异大 | CQRS | 普通 CRUD |
| 需要完整事实历史/重放 | Event Sourcing | 只为审计日志 |
| 重复请求/消息不能重复副作用 | Idempotency | 天然幂等读操作 |
停止条件:保持简单
命中任一条件时推荐"不使用模式":
- 只有一个实现,且未来变化无证据。
- 分支少、稳定、可读,抽象后跳转成本高于收益。
- 新模式引入状态/异步/持久化复杂度,但无对应可靠性或一致性需求。
- 团队无法测试、观测或运维该模式(Event Sourcing、Saga、复杂事件流)。
- 语言或框架已有原生能力:命名参数、DI 容器作用域、平台服务发现、K8s Service。
- 根因是职责划分或数据模型错误——先修边界,不要用模式掩盖。
- 为了消除一个很小很稳定的
if引入多个接口和类;把模式数量当质量指标。
Gotchas
- 同名不同物:
enterprise/repository.md是集合式持久化抽象(适合 CRUD/查询封装),ddd/repository.md是聚合根级仓储(富领域模型、强事务边界)。选错会让 ORM 类型渗进领域层。 - Strategy vs State:Strategy 由调用方/注册表选择,策略之间互不感知;State 由状态对象自己触发迁移并约束合法转换。
- Decorator vs Proxy:Decorator 动态叠加职责、调用方知道包装存在;Proxy 控制访问(权限、事务、懒加载、远程替身),真实对象对调用方不可见。
- Outbox ≠ exactly-once:只保证至少一次投递,消费端必须按业务键幂等。
- Saga 补偿 ≠ rollback:补偿是另一个业务动作,本身会失败,需要重试、可观测和人工兜底。
- CQRS 与 Event Sourcing 不绑定:可各自独立采用,不要默认成对引入。
- Singleton 优先用 DI 容器作用域,不要手写全局静态实例——那是隐藏的全局状态和测试难题。
- Adapter 不负责业务编排:适配层只做类型/协议转换,否则会变成第二个服务层。
- 超时先于重试:多层无预算重试会放大流量,必须有总预算和退避上限。
输出模板
## 压力
<具体代码/架构压力;变化频率、失败后果、当前已产生的成本>
## 结论
<主模式>(必要时:<辅助模式>),或明确写「不引入模式,保持 <更简单的做法>」
记录:<docs/design-decisions/NNNN.md>(本次是新建 / 追加 / 取代 / 废弃)
## 理由与排除
- 命中信号:<可观察证据,不是"最佳实践">
- 排除 <最相近候选>:<为什么>
## 最小落地
角色 / 接口所在层 / 创建与装配位置 / 一次请求或消息的调用时序
## 成本与退出条件
<新增间接层、状态、网络跳数、存储、运维负担>;<需求规模下降或假设不成立时如何回退>
## 验证
<不变量测试、边界/并发/重试用例、需要打的指标与日志>
异步模式(事件、Outbox、Saga、CQRS、重试)必须在「验证」里写明重复、乱序和恢复策略。
设计决策日志(可迭代)
设计结论必须沉淀成记录,随需求变更演进。历史只增不改:旧记录保留,靠状态和「变更历史」表达演进。
位置:优先沿用项目已有约定(docs/adr/、docs/decisions/、ADR/、已有设计日志);都没有则用 docs/design-decisions/,索引为 INDEX.md。
创建记录:
python3 scripts/new_decision.py "支付渠道分支改用 Strategy" # 自动编号 + 更新索引
python3 scripts/new_decision.py "订单状态机改用 State" --supersedes 0001
脚本从 assets/decision-record-template.md 生成骨架(压力 / 候选与排除 / 结论 / 最小落地 / 成本与退出条件 / 验证 / 变更历史),填内容即可。编号永不复用,不覆盖已有文件。
需求变更时的三种动作:
| 情况 | 动作 |
|---|---|
| 结论不变,只是约束/规模变化 | 在原记录「变更历史」追加一行:日期、需求变更、结论是否变 |
| 结论变了 | 新建记录并 --supersedes <原编号>;原记录状态改为「已被取代」,保留原文 |
| 命中原记录的「退出条件」 | 原记录状态改为「已废弃」,新建记录写明回退方案与回退后的更简单设计 |
复盘触发:每次需求变更后,重扫相关记录的「退出条件」。命中就回退,不要因为已经实现过就继续保留模式。禁止删除或静默覆盖旧记录,也禁止只改结论不写理由。
采纳前自检
输出建议前确认:
- 压力是真实的(已有 ≥2 个实现/分支,或已产生回归),不是"未来可能"。
- 已比较至少一个相近候选并说明排除理由。
- 更简单方案(函数、参数、组合、平台能力)已被明确排除。
- 团队能测试和观测新增间接层;异步模式已说明重复/乱序/恢复。
- 结论已落入设计决策日志:新建记录,或在既有记录上追加变更/取代/废弃。
任一项未通过 → 回到「停止条件」,优先保持简单。
模式索引
GoF / 创建型
GoF / 结构型
GoF / 行为型
- Chain of Responsibility · Command · Interpreter · Iterator · Mediator · Memento · Observer · State · Strategy · Template Method · Visitor
企业应用模式
- Repository · Service Layer · Unit of Work · Data Mapper · Active Record · DTO · Dependency Injection · MVC
DDD 模式
- Entity · Value Object · Aggregate · Aggregate Root · DDD Repository · Domain Service · Domain Event · Application Service · DDD Factory · Specification