Java 设计模式(策略 / 适配器 / 模板方法 / 工厂方法 / 建造者)
前言
分享下项目经常用到的几种策略模式,完整的系统,这几种策略模式一般都是会同时用到的,特别是模版方法+策略+适配的组合编码,试着从自己项目中找到这几类设计模式的应用吧。
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 接口,具体实现 LocalFileStore、S3FileStore、MinioFileStore。
如果直接 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. 结尾
如果今天只记住一句话:模式不是为了“看起来高级”,而是为了降低变化成本。
策略解决算法变化,适配器解决兼容问题,模板方法解决流程复用,工厂方法解决创建解耦,建造者解决复杂对象构建。
在真实项目里,它们往往不是单独出现,而是组合使用。你们做技术方案时,不必先问“用什么模式”,而要先问“变化点在哪里、风险在哪里、如何最小代价演进”。
只要能回答这三个问题,你就已经在用设计模式思维做工程了。
更多推荐




所有评论(0)