设计模式在 Java 中的正确打开方式——不要为了模式而模式
设计模式在 Java 中的正确打开方式——不要为了模式而模式
适读人群:Java 开发者,尤其是刚背完设计模式准备大展身手的同学 | 阅读时长:约16分钟 | 核心价值:帮你从"套模式"的思维转向"解问题"的思维
我有个判断:一个工程师刚学完设计模式之后,是最危险的。
不是说学了不好,而是刚学完之后有一种"手里拿着锤子,看什么都像钉子"的状态。工厂模式、策略模式、装饰器模式……看着代码库,脑子里开始各种套:这里能用建造者,那里可以加个观察者,这个条件判断可以换成责任链……
然后代码就越写越重,越写越难懂,每次改需求都要改一堆地方,跑过来问我说"我明明用了设计模式,为什么代码还是这么乱?"
这篇文章我想认真讲讲,设计模式到底是什么,我在 Java 项目里怎么真正用它,以及更重要的——我什么时候决定不用。
设计模式的本质是什么
我见过很多教程,把设计模式讲成一套"代码写法"——工厂模式就是写个工厂类,策略模式就是抽个接口然后搞若干实现。
这种理解是错的,或者说是不完整的。
设计模式本质上是一套沟通词汇,也是一套应对特定变化的方案。
GoF 那本书(《设计模式》)写在1994年,当时面向对象编程还是新事物。作者们观察了大量实际的面向对象代码,发现有些特定的"结构"反复出现,而且这些结构在应对某类变化时特别有效——于是他们把这些结构总结归纳,起了名字,让工程师之间可以用简短的词汇来沟通复杂的结构。
所以设计模式有两个维度:
- 它解决的是什么问题(或者说,应对的是什么变化)
- 它的代价是什么(抽象程度增加,代码量增加,理解成本增加)
大多数人只学了第一个维度,没有认真思考第二个维度。
一个反面教材
我们团队曾经有个同学,非常喜欢设计模式。他做了一个积分系统,用了工厂模式 + 策略模式 + 模板方法 + 责任链,整个系统大概6个类,最终代码量大概1500行。
我接手维护这个系统的时候,花了将近两天才把整个流程理清楚,因为要不断在类之间跳来跳去,看接口实现,看工厂创建,看链式调用……
最令我崩溃的是:这个积分系统,其实就三种积分类型,而且业务方明确说了"目前没有扩展计划"。
换一个思路,如果用枚举 + 简单 if-else,可能300行就搞定了,而且任何人看了5分钟就能明白整个流程。
那个同学的问题不是"不懂设计模式",而是"在一个不需要设计模式的地方强行套了设计模式"。
我判断是否需要用设计模式的方法
我的核心问题是:这里是否存在需要被隔离的变化点?
设计模式的核心价值,是把"会变化的部分"和"不会变化的部分"隔离开来。如果一段代码压根就不需要变化,或者即使变化了改起来也不复杂,那引入设计模式只会增加理解成本,没有任何好处。
我的判断标准:
用设计模式的时机:
- 这里有多个变体,而且变体的数量还会增长
- 这里的逻辑需要在运行时动态决定
- 这里的变化会频繁发生,而且不同的变化需要独立修改,互不干扰
- 团队里有多人在同时维护这块代码,需要减少冲突
不用设计模式的时机:
- 逻辑简单,一个 if-else 能解决,而且条件数量不会增长
- 这是一次性代码,写完之后不会再动
- 团队规模小,代码是一个人维护,没有沟通成本
- 业务明确说了"不会有扩展需求"
我在真实项目里用过的几个模式
策略模式——我用得最多的一个
场景:我们有一个支付系统,支持微信、支付宝、银行卡、苹果支付。每种支付方式的调用方式、回调处理都不一样,但上层的调用逻辑(下单、支付、确认、退款)是一样的。
这里有一个明显的变化点:支付渠道会增加(后来果然加了企业微信支付和字节的支付)。而且不同渠道的实现需要独立修改,不应该互相影响。
这是一个标准的策略模式适用场景。
我的做法:
public interface PaymentStrategy {
PayResult pay(PayRequest request);
RefundResult refund(RefundRequest request);
boolean supports(String channelCode);
}@Component
public class WechatPayStrategy implements PaymentStrategy {
// 微信支付的具体实现
}@Component
public class AlipayStrategy implements PaymentStrategy {
// 支付宝的具体实现
}然后用一个 PaymentStrategyRegistry 来管理所有策略,根据渠道码查找对应的策略。这样每次加新渠道,只需要新增一个实现类,注入进去,完全不需要改现有代码。
注意:这里我特意没有用工厂模式,因为 Spring 的 IoC 容器本身就能完成依赖注入和对象管理,再加一个工厂是多此一举。这是一个"用了 Spring 之后很多模式可以简化"的典型案例。
观察者模式——事件驱动的天然搭档
场景:订单支付成功之后,需要做一堆事情:发通知、更新库存、触发发货、记积分、更新会员等级。这些事情之间没有强依赖,而且未来还会增加新的"支付成功后要做的事"。
如果直接在支付成功的方法里依次调用:
public void onPaySuccess(Order order) {
notificationService.sendPaySuccessNotification(order);
inventoryService.deductInventory(order);
shippingService.triggerShipping(order);
pointsService.addPoints(order);
memberService.updateMemberLevel(order);
// 未来还会有更多...
}这个方法会越来越长,而且每加一个新逻辑,都需要改这个方法,违反了开闭原则。
用观察者模式(或者 Spring 的事件机制)来处理这个场景非常合适:
// 支付成功后,发布事件
applicationEventPublisher.publishEvent(new OrderPaidEvent(order));
// 各个监听者独立处理
@EventListener
public void onOrderPaid(OrderPaidEvent event) {
// 各自的逻辑
}这样每个"支付成功后要做的事"都是一个独立的监听器,新增需求就新增监听器,完全不需要改核心支付流程。
我在实际使用中,会进一步把一些非核心的、允许异步的逻辑放到 @Async 监听器里,比如发通知、记积分,这样不会阻塞主流程。
建造者模式——但不是你想象的那种
很多人用建造者模式是因为"构造函数参数太多"。这种理解没错,但我觉得建造者模式的更大价值是:构建复杂对象的过程是可变的。
在一个报表系统里,我们需要根据不同的配置生成不同格式的报表(Excel、PDF、CSV),而且每种格式还有不同的样式配置(标题行、颜色、字体大小)。这里用建造者模式就很合适:
Report report = new ReportBuilder()
.format(ReportFormat.EXCEL)
.title("月度销售报表")
.addSheet("销售数据", salesData)
.addSheet("趋势分析", trendData)
.withTheme(Theme.CORPORATE)
.build();这个 Builder 内部封装了复杂的对象构建过程,调用方不需要关心报表对象是怎么一步步组装起来的,只需要告诉它"我要什么"。
装饰器模式——AOP 的穷人版
场景:有一个数据查询接口,需要加缓存。但是这个接口以后可能还要加限流、加日志、加权限校验,而且这些功能是正交的——有的接口需要缓存但不需要限流,有的接口需要所有功能。
用装饰器模式来处理:
DataService base = new DataServiceImpl();
DataService withCache = new CachedDataService(base);
DataService withRateLimit = new RateLimitedDataService(withCache);
DataService withLog = new LoggedDataService(withRateLimit);当然,在 Spring 项目里,这类横切关注点通常用 AOP 来处理更优雅。但在没有 Spring 的场景,或者需要细粒度控制组合方式的场景,装饰器模式是很好的选择。
我决定不用模式的几个典型 case
Case 1:简单枚举搞定的,不用工厂
有同学建议用工厂模式来创建不同类型的消息对象。我看了下,消息类型就3种(短信、邮件、推送),而且创建逻辑就是 new XXXMessage(params),完全可以用枚举映射或者 switch 解决,没必要搞工厂类。
后来那位同学说:"但是如果以后增加了微信消息怎么办?"
我说:"等真的要加的时候再重构,现在加一个 case 大概10行代码,比你花两天设计工厂体系要合算得多。"
这是 YAGNI 原则:You Aren't Gonna Need It。不要为了一个"可能发生"的扩展需求,现在就引入复杂度。
Case 2:Spring 已经做了,不要自己造
责任链模式在很多场景里可以直接用 Spring 的拦截器链、过滤器链来实现,没必要自己写。 单例模式在 Spring Bean 默认就是单例的,没必要手写 getInstance()。 工厂模式的很多场景,直接用 @Qualifier + @Autowired 就能解决。
了解 Spring 到底帮你做了什么,可以避免很多不必要的重复建设。
Case 3:业务逻辑太简单,强行套模式只会变蠢
有次有人给我 review 代码,一个计算运费的方法,根据重量和地区算运费,就3个条件判断,写成了策略模式 + 工厂 + 枚举,总共4个文件,500行代码。
如果直接写,就是:
public BigDecimal calculateShipping(double weight, String region) {
if ("新疆".equals(region) || "西藏".equals(region)) {
return BigDecimal.valueOf(weight * 15 + 20);
}
if (weight > 10) {
return BigDecimal.valueOf(weight * 8 + 10);
}
return BigDecimal.valueOf(weight * 6 + 5);
}20行,逻辑清晰,任何人一眼就明白。
设计模式的学习建议
很多人背完了23种设计模式,但依然不知道什么时候用。我认为原因是学习路径反了。
正确的学习路径应该是:先遇到问题,再去找模式,而不是"先把模式背下来,再找地方套"。
去找一些真实的开源项目(比如 Spring 源码、Netty 源码),看它们在哪些地方用了哪些模式,为什么要用,解决了什么问题。这比对着书本背定义有效十倍。
当你在做项目时遇到"这段代码好像不对,但说不清楚哪里不对"的感觉时,去翻设计模式,很可能能找到对应的解法。这种主动寻找的学习方式,远比被动记忆更有效。
最后,我最推崇的一种评价代码的方式不是"有没有用设计模式",而是"三个月之后,一个没接触过这段代码的工程师,能不能在20分钟内理解它的意图"。
能做到这一点,不管用没用设计模式,都是好代码。
