前言

分享下项目经常用到的几种策略模式,完整的系统,这几种策略模式一般都是会同时用到的,特别是模版方法+策略+适配的组合编码,试着从自己项目中找到这几类设计模式的应用吧。

1. 策略模式

1.1 从业务痛点开始:为什么一定会遇到策略模式

真实项目里,最常见的坏味道是一个“核心服务类”塞满分支:if (type == A) ... else if (type == B) ...。一开始只有两个规则时你会觉得非常快,甚至觉得“没必要抽象”。但半年之后,业务变成 A1/A2/B1/B2/灰度实验规则,再加上渠道、用户等级、地区、节假日,分支会呈乘法爆炸。
这种爆炸有三个直接后果:
1)发布风险上升:你改 B 规则,A 规则也可能被影响;
2)认知成本上升:新人不敢改这个类;
3)测试成本上升:一个方法里几十条路径,边界覆盖几乎不可能。
策略模式本质上是“把变化点从主流程剥离出去”。主流程只负责“什么时候用策略”,不负责“策略内部怎么算”。这样业务扩展就从“改旧代码”变成“新增策略类 + 注册策略”,风险可控很多。

1.2 结构推导:三层角色怎么分工

策略模式看起来只是接口+实现,但真正好用的版本通常包含四层:

  • Strategy:定义统一能力边界;
  • ConcreteStrategy:实现一个规则;
  • Context:持有并执行策略;
  • StrategyRegistry(可选):把策略和业务 key 绑定。

很多团队只写前三层,后期仍然会写一段 if-else 来选择策略,这会让“策略模式只完成一半”。建议把“选择策略”也工程化:

  • 可以用 Map 注册;
  • 可以结合 Spring @Component + @Qualifier
  • 可以按配置中心动态切换(灰度策略非常常见)。

1.3 进阶案例:支付路由与优惠计算双策略组合

在电商下单里,通常有两类变化:

  • 路由策略:走哪个支付网关;
  • 价格策略:按什么规则计算最终价。
    这两个变化维度不要混在一个策略里,应该拆分为两个策略族,然后在 OrderCheckoutContext 中组合。这样你可以得到“网关策略 * 价格策略”的灵活组合,而不是写成上百个 if 分支。
    你在分享时可以强调一句:策略模式不是只有一个策略维度,多个独立变化点可以多个策略族并行。

1.4 常见反模式(非常重要)

1)伪策略:只是把 if 分支挪到策略类里,选择时仍然写 if。
2)策略泄漏上下文:策略实现频繁访问数据库、远程接口,导致不可预测。
3)策略职责过大:一个策略里既做校验、又做转换、又做持久化。
4)策略无版本管理:线上灰度策略回滚困难。

修复建议:

  • 给策略输入输出做明确 DTO;
  • 策略只做“规则计算”,IO 留给外层;
  • 策略命名标准化(如 VipPriceV2Strategy);
  • 把策略选择日志打全(便于审计和排障)。

1.5 测试设计:如何让策略模式可验证

策略模式的测试最容易做“单策略单测”,但分享里要强调两层测试:

  • 单策略单测:验证每个策略在边界输入下结果正确;
  • 选择逻辑测试:验证 key -> strategy 的映射正确;
  • 集成测试:验证策略组合(例如支付策略 + 优惠策略)对账一致。
    建议再加一条:把历史线上真实输入做成回放样本,防止规则升级出现“静默漂移”。

1.6 评审清单

  • 变化点是否独立且稳定?
  • 新增策略是否无需改旧策略代码?
  • 是否存在策略选择日志与埋点?
  • 错误策略是否有兜底策略(NullObject/Fallback)?
  • 策略是否被过度拆分导致类过多难维护?

1.7 面试/分享高频问答

  • 问:策略和状态模式区别?
    答:策略是外部选择算法;状态模式是对象内部状态驱动行为切换。
  • 问:策略是不是一定比 if 好?
    答:不是。分支少且稳定时,if 更直接;变化频繁才值得上策略。
  • 问:策略很多怎么治理?
    答:注册中心 + 命名规范 + 统一监控 + 生命周期管理。

2. 模板方法模式

2.1 模式本质:流程稳定,步骤变化

模板方法模式并不是“为了继承而继承”,它解决的是流程型业务的稳定性问题。
当你有明确、长期稳定的主流程,例如“校验 -> 处理 -> 记录 -> 通知”,但每个步骤的细节会因渠道或场景不同而变化,模板方法非常适合。
如果你把流程写在每个子类里复制粘贴,最终会出现步骤顺序不一致、审计漏打、异常处理不统一等线上事故。

2.2 设计原则

  • 模板方法一般 final,防止子类破坏流程骨架;
  • 可变步骤用 abstract
  • 钩子步骤(hook)给默认实现,子类可选覆盖;
  • 公共前后处理(日志、审计、异常包装)放父类统一。

2.3 实战案例:文件导出平台

导出主流程固定:
1)参数校验;
2)权限验证;
3)查询数据;
4)格式化(CSV/Excel/PDF);
5)上传文件;
6)回写任务状态。
变化点在“格式化”和“上传策略”,父类控制大流程,子类只做差异化。
这可以显著避免“某个导出类型忘记鉴权”的高风险问题。

2.4 异常与事务处理建议

模板方法很适合统一异常语义:

  • 子类抛业务异常,父类转换为统一错误码;
  • 父类决定是否重试、是否补偿;
  • 父类统一写审计日志。
    如果流程涉及事务边界,建议父类控制事务入口,子类只提供纯业务步骤,避免事务分裂。

2.5 反模式

1)父类过胖:父类知道太多业务细节,演变成“上帝类”。
2)模板不稳定:流程本身经常改,说明抽象时机太早。
3)强行继承:其实更适合组合+策略,却硬上模板方法。
4)钩子滥用:钩子过多导致执行路径不透明。

修复思路:

  • 父类只放稳定流程和公共横切;
  • 不稳定步骤转为策略注入;
  • 钩子数量控制,必须配文档。

2.6 模板方法 + 策略组合

强烈建议在分享中讲这个组合:

  • 模板方法控制大流程;
  • 策略负责步骤内部算法。
    这在支付、风控、审批系统里非常常见,也比纯继承更灵活。

2.7 测试策略

  • 父类流程测试:验证步骤顺序(可用 spy/mock);
  • 子类步骤测试:验证每个变化步骤边界;
  • 异常流程测试:验证父类兜底和审计必达。
    核心目标是“流程正确 + 变化可控”。

3. 工厂方法模式

3.1 为什么“new”会成为技术债

业务代码里直接 new 具体类,短期快,长期会让调用方与实现强耦合。
一旦你要切换实现(比如短信服务从供应商 A 切到 B),就得全局搜索替换,风险极高。
工厂方法模式就是把“创建逻辑”独立出来,让调用方依赖抽象产品,而非具体产品。

3.2 结构理解

  • Product:产品抽象
  • ConcreteProduct:具体产品
  • Creator:工厂抽象(定义工厂方法)
  • ConcreteCreator:具体工厂(决定创建哪种产品)

跟简单工厂相比,工厂方法把“分支创建逻辑”分散到子类,避免一个超级工厂不断膨胀。

3.3 业务案例:多存储后端切换

你有 FileStore 接口,具体实现 LocalFileStoreS3FileStoreMinioFileStore
如果直接 new,业务层到处耦合具体实现。
改成工厂方法后,业务层只拿 FileStoreFactory#create() 结果使用。
切环境、切云厂商、灰度迁移都更自然。
配合配置中心后,甚至可以按租户选择不同工厂实现。

3.4 工厂方法与依赖注入(Spring)关系

很多人问“用了 Spring 还需要工厂吗?”
答案:需要,场景不同。

  • DI 解决对象生命周期与注入;
  • 工厂方法解决“业务语义上的创建决策”。
    例如同是 Notifier,你可能要按用户偏好或故障熔断结果选择不同实现,这仍然是工厂职责。

3.5 反模式

1)工厂只是 new 一下,没有任何创建策略价值。
2)工厂和产品一一对应但无扩展预期,徒增类数量。
3)工厂里又塞 if-else,回到简单工厂老路。
4)调用方仍然 instanceof 判断具体实现,抽象失效。

3.6 落地建议

  • 把创建策略写成配置化规则;
  • 工厂输出统一抽象,不向外暴露具体类型;
  • 新增产品必须新增工厂测试;
  • 工厂日志记录“为什么选了这个实现”。

3.7 与抽象工厂区别

  • 工厂方法:创建“一个产品等级结构”对象。
  • 抽象工厂:创建“一组相关对象(产品族)”。
    分享时可一句话:一个工厂一个产品 vs 一个工厂一套产品

4. 适配器模式

4.1 业务背景:系统演进必然出现“接口断层”

适配器模式最常见于系统升级阶段:老系统 SDK 仍在用,新平台定义了统一能力接口。你不可能一次性重写所有存量系统,也不能强行让业务方理解 N 套第三方协议。
这时候最优解不是“直接改业务代码调用旧 SDK”,而是建立统一接口,再通过适配器把旧 SDK 包进去。这样业务层感知的是标准接口,兼容成本被锁在适配层。

4.2 设计重点:适配器不仅做参数转换

很多同学把适配器理解为“改下参数名”,实际上生产级适配器要做四类转换:
1)数据结构转换(DTO/字段映射);
2)语义转换(返回码->异常;null->空对象);
3)时序转换(同步调用改成异步回调或反之);
4)可靠性转换(重试、幂等、超时兜底)。
也就是说,适配器是“协议翻译层”,不是简单 wrapper。

4.3 实战案例:统一对象存储接口接入三家云厂商

假设你定义 ObjectStorageClient 接口:put/get/delete
阿里云、腾讯云、AWS 的 SDK 入参、异常、签名方式都不一致。
做法:每家写一个 Adapter,统一转换为平台标准异常和统一元数据结构。
收益:上层业务只依赖平台接口,后续新增厂商只加一个适配器类,不改业务代码。

4.4 反模式与风险

1)适配器中塞业务逻辑:本应在服务层的规则被塞进 adapter,边界污染。
2)适配器过深链路:Adapter 套 Adapter,排查时很痛苦。
3)错误语义未统一:有的返回 false、有的抛异常,调用端误判。
4)忽略可观测性:出问题不知道卡在哪一层。

治理建议:

  • 适配层统一错误码/异常体系;
  • 每个适配器增加请求耗时与失败率指标;
  • 统一 trace tag:adapter=xxx
  • 适配器单测必须包含第三方返回异常/超时场景。

4.5 类适配器 vs 对象适配器

Java 中优先对象适配器(组合),因为:

  • 单继承限制导致类适配器扩展受限;
  • 组合更容易做运行时替换和 mock;
  • 组合更适合 Spring 依赖注入。
    除非你有强约束且继承关系极清晰,否则不建议类适配器。

4.6 和外观模式区别(容易混淆)

  • 适配器:为“接口不兼容”而生。
  • 外观:为“简化复杂子系统调用”而生。
    一句话:适配器解决“能不能接上”,外观解决“好不好用”。

4.7 迁移路线建议

1)先定义标准接口;
2)给旧系统写 adapter;
3)新代码只能依赖标准接口;
4)逐步替换旧调用;
5)最后删除历史直连路径。
这就是典型“绞杀者迁移”策略,风险低、可渐进。


5. 建造者模式

5.1 业务痛点:构造函数参数灾难

当对象参数超过 5 个,尤其包含多个可选参数时,构造函数会变得不可读。
new Task("A", true, 3, null, "csv", 100, false, "x") 这种调用几乎无法一眼看懂。
更危险的是参数顺序错位,编译期还可能不报错(类型刚好兼容),上线后出现隐蔽 bug。
Builder 的核心价值是“可读、可校验、可扩展”。

5.2 推荐实现规范

  • 外部类字段 final(尽量不可变);
  • Builder 提供链式设置;
  • build() 做完整校验;
  • 提供合理默认值;
  • 必填参数可放 Builder 构造器中强制输入。

5.3 实战案例:查询请求对象构建

复杂查询常包含:分页、排序、过滤、时间区间、租户、数据权限、超时策略。
这些参数在不同调用场景差异很大,Builder 可以避免“重载构造器爆炸”。
你还可以给 Builder 增加语义化方法:forTenant()withPermissionScope(),让业务代码更像 DSL,读起来更直观。

5.4 Builder 与 Lombok @Builder

Lombok 可以快速生成 Builder,但分享时建议提醒:

  • 生成快不等于设计好;
  • 必填校验、跨字段约束(比如开始时间 <= 结束时间)仍需手写;
  • 复杂对象建议保留手写 Builder,表达业务语义更清晰。

5.5 反模式

1)简单对象也用 Builder:过度工程化。
2)可变对象 + Builder 混用:创建后仍然随意 set,失去价值。
3)build 不校验:把错误延迟到运行时深处。
4)Builder 直接泄漏内部可变集合:线程安全与封装问题。

5.6 与工厂方法的关系

两者不冲突,常常组合使用:

  • 工厂方法决定“创建哪一类对象”;
  • Builder 负责“把这个对象如何组装完整”。
    例如:工厂先选中 ExcelExportTask,再用 Builder 设置分页、压缩、重试、输出目录。

5.7 测试与评审清单

  • build() 是否覆盖必填参数校验?
  • 默认值是否符合业务预期?
  • 构建后对象是否不可变?
  • 字段间约束是否在构建期验证?
  • 链式 API 命名是否可读、无歧义?

6. 结尾

如果今天只记住一句话:模式不是为了“看起来高级”,而是为了降低变化成本。
策略解决算法变化,适配器解决兼容问题,模板方法解决流程复用,工厂方法解决创建解耦,建造者解决复杂对象构建。
在真实项目里,它们往往不是单独出现,而是组合使用。你们做技术方案时,不必先问“用什么模式”,而要先问“变化点在哪里、风险在哪里、如何最小代价演进”。
只要能回答这三个问题,你就已经在用设计模式思维做工程了。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐