Java设计模式实战:单例、工厂、策略、观察者四大模式详解
设计模式这四个字在 Java 圈子里一直很有话题性。面试要考晋升要讲代码评审还动不动被点名“这里是不是该用个策略模式”。可奇怪的是很多人啃完书、背完二十几个模式的类图回到工位写业务代码照样是 if-else 满天飞一个模式也用不上。我早年也是这么过来的直到后来在真实项目里被代码结构折磨到怀疑人生才真正想明白一件事设计模式不是考试知识点而是前人把常见问题踩过一遍之后总结出来的“解法套路”。你不需要全背下来只需要在代码开始变烂的时候知道该掏哪一把工具。这篇文章我只聊四个在 Java 日常开发里出现频率最高、性价比也最高的模式单例、工厂、策略、观察者。它们分别处理对象创建、依赖解耦、算法替换、事件通知这四类最常见的代码问题。每一个我都会给出完整的 Java 代码示例、适用场景、边界判断以及我在实际项目中踩过的坑。这篇文章适合刚工作一到三年的 Java 开发者精读也适合想快速回顾模式实战的老手直接跳着看重点部分。1. 先想明白设计模式到底在救什么火1.1 设计模式不是什么高大上的东西很多初学者把设计模式当成某种“高级编程技巧”总觉得代码里用了设计模式就显得有水平。这个观念得先纠正过来。设计模式本质上就是一套被反复验证过的代码结构方案它解决的是软件开发里最朴素的问题代码改起来太费劲。你想想任何一个项目刚写完的时候都挺好功能都能跑。真正让人头疼的是三个月后改需求的时候改 A 功能结果 B 功能蹦了加一个新类型要动十几个类测试用例写起来又慢又脆。设计模式就是为了对付这些“慢性病”的。打个比方。你在家里做饭盐罐子放在灶台右手边顺手的位置这是你总结出来的“摆放模式”。没有这个模式你也能做饭但每次炒菜都要到处翻盐罐子时间一长你就烦了。设计模式就是软件世界的“顺手放盐罐子”它不改变你做菜的本质但让你做得更顺、改起来更快。所以判断一个模式好不好唯一的标准就是用了它之后代码是不是更容易改、更容易测、更容易看懂。1.2 用三个信号判断代码该不该上模式我不建议一上来就对着类图硬套。我自己的习惯是先闻代码的味道出现下面这三个信号之一再考虑上模式。第一个信号是 if-else 或者 switch 分支长得离谱。比如一个方法里根据订单类型、会员等级、支付渠道做三层嵌套判断随便加一个分支就要穿透整个方法。这种情况十有八九能用策略模式或者工厂模式把它们拆开。第二个信号是改一个功能要动很多地方。比如你把用户状态从“启用/禁用”改成“启用/禁用/锁定”结果查代码发现十几个地方都在判断这个状态每个都要改。这说明状态判断的逻辑没有被收敛到一个地方需要想办法封装。第三个信号是单元测试写不动。一个方法里既 new 了数据库连接又调了远程接口还要写文件你想给它写测试都不知道怎么 mock。这通常说明依赖太散、耦合太紧需要引入工厂或者依赖注入把依赖收敛起来。有任何一个信号出现模式才有登场的必要没有信号硬上模式那就是为了用而用往往会把简单的代码改复杂。记住这一点下面四个模式看起来才会更有感觉。1.3 为什么偏偏是这四个模式设计模式一共有二十多个常见的也有十几个我为什么只挑了这四个因为它们在电商、后台管理系统这类业务开发里最常出现也是我看过大量开源框架源码之后发现出现频次最高的四个。单例负责“全局只此一份”工厂负责“创建对象不散落”策略负责“算法随便换”观察者负责“事件通知不耦合”。这四个正好覆盖了 Java 业务开发里遇到最多的四类结构问题而且代码量都不大学完当天就能用上。后面的章节我按“问题场景 - 代码实现 - 适用边界 - 踩坑记录”的顺序逐个讲。每个模式我都会先从一段有问题的真实代码切入这样你能先感受到痛点再看到模式是怎么把痛点消掉的。2. 单例模式最简单也最容易写歪2.1 单例模式到底在解决什么问题单例模式保证一个类在整个 JVM 进程里只有一个实例并且提供一个全局访问入口。它的使用场景非常直白有些对象创建起来很贵或者本身就不应该有多个副本。比如数据库连接池、线程池、配置中心客户端、日志器这些对象如果每个调用方都 new 一个资源直接被打爆。举个例子一个配置对象要从远端的配置中心拉数据拉一次就要走一次网络。如果每个业务类都 new 一个配置对象一次请求过来好几个类各自拉一遍配置性能就废了。单例模式让所有人都拿到同一个实例配置只加载一次大家共用。这也是为什么几乎所有框架的配置类、工具类都用单例的方式管理因为确实没必要一个调用方一个副本。2.2 四种写法从能用写到好用单例模式看起来简单写起来却有不少讲究。我见过很多写法这里把最典型的四种列出来说说各自的优缺点。第一种饿汉式。类加载的时候就把实例创建好public class ConfigManager { private static final ConfigManager INSTANCE new ConfigManager(); private ConfigManager() { // 初始化配置 } public static ConfigManager getInstance() { return INSTANCE; } }这种写法简单、线程安全缺点是不能懒加载。如果这个类创建很重而且项目启动时不一定会用到就会白白浪费启动时间。但对于绝大多数配置类、工具类来说饿汉式其实是够用的别为了那一点点懒加载的收益把代码搞复杂。第二种懒汉式加锁。第一次调用时才创建public class ConfigManager { private static ConfigManager instance; private ConfigManager() {} public static synchronized ConfigManager getInstance() { if (instance null) { instance new ConfigManager(); } return instance; } }这种写法用 synchronized 保证线程安全但每次调用 getInstance 都要抢锁。在高并发调用场景下会有性能损耗虽然大多数业务对单例的调用频率没那么夸张但确实算不上最优解。第三种双重检查锁。既懒加载又只在第一次创建时锁public class ConfigManager { private static volatile ConfigManager instance; private ConfigManager() {} public static ConfigManager getInstance() { if (instance null) { synchronized (ConfigManager.class) { if (instance null) { instance new ConfigManager(); } } } return instance; } }注意这个 volatile 一定不能省。如果没有它另一个线程可能看到一个“半初始化”的实例——对象已经分配了内存但构造方法还没跑完。这个坑早年坑过不少人面试也爱考。双重检查锁是目前手写单例里比较推荐的方案兼顾了懒加载和性能。不过有一点要提醒它的代码读起来稍微绕一点如果团队里都是刚入门的同学用饿汉式反而更稳妥。第四种枚举单例。这是《Effective Java》里作者强烈推荐的方式public enum ConfigManager { INSTANCE; private String config; ConfigManager() { // 初始化配置 } public String getConfig() { return config; } }枚举写法天生线程安全还能防止反射和序列化破坏单例。唯一的“缺点”是有些人看着不习惯觉得枚举里写业务逻辑怪怪的但实际用起来非常稳。写框架、写公共组件时我优先用枚举单例省心。下面这个表把这四种写法放在一起对比方便你按场景选写法懒加载线程安全防反射/序列化破坏推荐度饿汉式否是否一般懒汉式加锁是是否低双重检查锁是是否高枚举否可配合内部类调整是是最高2.3 单例不是“全局变量”的遮羞布单例好用但很容易被滥用。我在代码评审里经常看到有人把本该传参的数据塞进一个单例对象里到处 get 和 set活生生把一个全局可变状态变成定时炸弹。判断该不该用单例我有一个简单的标准看这个对象是否“无状态”或者“状态只读”。数据库连接池、配置中心客户端状态是连接信息、配置内容基本只读适合单例。但像“当前用户会话”“购物车数据”这类持有大量可变业务状态的对象你把它做成单例所有线程都在往同一个对象里写数据迟早写出并发问题。另外单例模式和我们日常用的 Spring 容器不是一回事。Spring 的 Bean 默认就是单例的但它是 IoC 容器创建和管理的容器帮你处理了创建时机和依赖注入你还可以通过 Scope 修改作用域。自己手写单例类主要用在不归 Spring 管的场景纯 Java 工具类、组件包、以及不用 Spring 的老项目。2.4 我在项目里踩过的单例坑有两个关于单例的坑我印象特别深。第一个是反射破坏单例。如果有人用反射调用私有构造方法饿汉式和双重检查锁写法的单例都会被绕过去new 出第二个实例。我在一个公共组件里被这样搞过一次排查了很久才发现是测试代码里有人用反射来做 mock。从那以后涉及核心组件的单例我优先用枚举写法至少在防反射这一项上能省心很多。第二个是序列化破坏单例。如果单例类实现了 Serializable反序列化时会生成一个新的实例单例就失效了。解决方法是加 readResolve 方法返回已有实例但这个细节太容易被忽略。我个人建议是能不加 Serializable 就不加非加不可时一定要测一下反序列化之后的对象是不是同一个。提示判断单例是否被破坏最简单的方法是打印两个“实例”的 hashCode。如果不一样说明你拿到的是两个不同的对象单例已经失效了。3. 工厂模式把 new 从业务代码里赶出去3.1 一段真实的“创建地狱”代码工厂模式解决的是“创建对象”这件事散落在业务代码各处的问题。我举个例子。假设你在做一个订单系统下单时要根据用户选择的支付渠道创建对应的支付服务一开始你可能这么写public class OrderService { public void pay(Order order) { Payment payment; if (channelA.equals(order.getChannel())) { payment new PayChannelA(); } else if (channelB.equals(order.getChannel())) { payment new PayChannelB(); } else if (channelC.equals(order.getChannel())) { payment new PayChannelC(); } else { throw new IllegalArgumentException(不支持的支付渠道); } payment.pay(order); } }这段代码的问题一眼就能看出来每加一个新的支付渠道就要改 OrderService 里的 if-else所有渠道的创建逻辑都堆在一起类越来越长。更麻烦的是如果以后创建支付对象时需要从配置中心读取密钥、初始化证书这段初始化逻辑也得塞进来OrderService 会被活活撑死。这种症状就是典型的“创建逻辑污染业务逻辑”。3.2 从简单工厂到抽象工厂三个级别一次讲清工厂模式不是只有一种常见的是三个层次简单工厂、工厂方法、抽象工厂。简单工厂就是把上面那堆 if-else 单独拆到一个类里去。创建逻辑收敛到一个地方业务代码只依赖工厂类public class PaymentFactory { public static Payment create(String channel) { if (channelA.equals(channel)) { return new PayChannelA(); } else if (channelB.equals(channel)) { return new PayChannelB(); } else if (channelC.equals(channel)) { return new PayChannelC(); } throw new IllegalArgumentException(不支持的支付渠道: channel); } }调用方变成一行Payment payment PaymentFactory.create(order.getChannel()); payment.pay(order);简单工厂的好处是改动集中缺点是工厂类里的 if-else 还是会越来越长。当渠道种类多到一定程度或者每个渠道的创建过程差异很大就该考虑工厂方法模式了。工厂方法模式把“创建对象”这件事定义成抽象方法让子类决定到底创建哪个具体对象public interface PaymentFactory { Payment create(); } public class ChannelAFactory implements PaymentFactory { Override public Payment create() { return new PayChannelA(); } } public class ChannelBFactory implements PaymentFactory { Override public Payment create() { return new PayChannelB(); } }调用方可以根据渠道选择对应的工厂。这种模式的好处是完全符合开闭原则新增渠道时只需要新增一个工厂类任何现有代码都不用改。当然代价是类变多了每个渠道多一个工厂类管理起来需要点纪律。抽象工厂则在工厂方法之上再抽象一层适合“创建一系列相关对象”的场景。比如支付服务里每个渠道除了支付对象还有对账对象、退款对象这时候就可以用抽象工厂把同一渠道的一整套对象都创建出来。业务开发中抽象工厂用得相对少一些了解概念即可。工厂类型创建逻辑集中符合开闭原则复杂度适用场景简单工厂是否低分支少创建逻辑简单工厂方法是是中分支多创建逻辑差异大抽象工厂是是高需要创建一系列配套对象3.3 工厂模式和框架是什么关系说到工厂模式很多框架其实已经把“工厂”做到了更高的层次。最典型的是 Spring 的 IoC 容器。你可以把 Spring 容器理解成一个超级工厂你只需要在配置里声明“我要一个什么样的 Bean”容器负责在合适的时机创建它、注入它的依赖、管理它的生命周期。所以我们写业务代码时并不需要真的去手写工厂类更多是借助 Spring 的依赖注入把对象交给容器管理。另一个典型的例子是 MyBatis 的 SqlSessionFactory它就是工厂模式的经典应用调用方不关心 SqlSession 是怎么创建的只要通过工厂拿一个可用的会话就行。包括 JDBC 里通过 DataSource 获取连接本质上也是工厂思想。这也是为什么我建议用 Spring 的开发者在学工厂模式时把重点放在“理解思想”而不是“手写工厂类”上。你在项目里更多的场景是把 Spring 容器当作现成的工厂来用。只要理解了工厂模式的核心——创建逻辑和业务逻辑分离——你写代码时就会下意识地知道哪些 new 该交给容器哪些该收敛到一个专门的类里。3.4 用工厂模式重构后长什么样用简单工厂重构那堆 if-else 之后业务代码变得干净得多但注意一个细节工厂类里的渠道判断不可避免。如果渠道名和 Bean 名能对上还可以用 Map 之类的方式替代 if-else。我在实际项目里更推荐的做法是结合 Spring 的依赖注入把一个渠道对应的支付 Service 都注入到一个 Map 里然后按渠道名直接取。这个玩法后面讲策略模式时还会再展开这里的核心是工厂模式帮你把“创建逻辑”从业务代码里剥离开至于创建时怎么判断手段可以灵活。注意工厂模式解决的是“谁来创建”的问题不要和策略模式搞混。如果一个对象创建出来之后你还要在多个算法之间频繁切换那是策略模式的活不是工厂模式的活。4. 策略模式干掉业务代码里最难缠的 if-else4.1 一段让人头皮发麻的折扣代码如果说工厂模式是收拾“创建对象”那策略模式就是收拾“根据条件选算法”。我见过最典型的场景是订单折扣计算。需求是这样的普通会员打九折黄金会员打八折铂金会员打七折大促期间全场再减十块。第一次写代码大概率长这样public double calculateDiscount(Order order) { double discount 0; if (NORMAL.equals(order.getMemberLevel())) { discount order.getAmount() * 0.1; } else if (GOLD.equals(order.getMemberLevel())) { discount order.getAmount() * 0.2; } else if (PLATINUM.equals(order.getMemberLevel())) { discount order.getAmount() * 0.3; } if (order.isPromotion()) { discount 10; } return discount; }刚写完的时候一切正常。两个月后产品经理说钻石会员要打六折大促减十块改成满一百减二十新会员首单再减五块。你看着这坨代码每加一个条件就要往方法里塞一层 if而且测试用例要覆盖所有分支的组合越写越崩溃。这种代码在业务项目里太常见了它的问题不是“跑得不对”而是“每次改动都像走钢丝”。4.2 策略模式的经典三件套策略模式通常由三部分组成策略接口、具体策略类、Context 上下文。先定义一个策略接口public interface DiscountStrategy { double calculate(Order order); }然后每个规则实现一个策略类public class NormalMemberStrategy implements DiscountStrategy { Override public double calculate(Order order) { return order.getAmount() * 0.1; } } public class GoldMemberStrategy implements DiscountStrategy { Override public double calculate(Order order) { return order.getAmount() * 0.2; } } public class PlatinumMemberStrategy implements DiscountStrategy { Override public double calculate(Order order) { return order.getAmount() * 0.3; } } public class PromotionStrategy implements DiscountStrategy { Override public double calculate(Order order) { return order.isPromotion() ? 10 : 0; } }Context 负责持有和调用策略。这里我把策略设计成一个列表让多个规则叠加生效更贴近真实业务public class DiscountCalculator { private final ListDiscountStrategy strategies; public DiscountCalculator(ListDiscountStrategy strategies) { this.strategies strategies; } public double calculate(Order order) { double total 0; for (DiscountStrategy strategy : strategies) { total strategy.calculate(order); } return total; } }你也可以把“选哪个策略”的逻辑放在 Context 里用一个类型字段决定用哪个策略。两种做法都没有绝对的对错关键是看“谁来决定用哪个策略”这件事要不要暴露给调用方。如果调用方自己知道该用哪个那 Context 只负责执行就够了。4.3 策略模式加 Spring 容器连判断都省了上面的代码解决了 if-else但还有一个问题调用方怎么知道自己该用哪个策略如果还是用 if-else 判断会员等级来选策略那 if-else 只是换了个地方存在并没有真正消失。Spring 的做法很优雅把每个策略注册成 Bean再用一个 Map 把它们收集起来按 key 取值Service public class DiscountStrategyContainer { private final MapString, DiscountStrategy strategyMap; public DiscountStrategyContainer(MapString, DiscountStrategy strategyMap) { this.strategyMap strategyMap; } public DiscountStrategy getStrategy(String memberLevel) { return strategyMap.get(memberLevel); } }前提是每个策略 Bean 都指定一个名字比如Component(GOLD) public class GoldMemberStrategy implements DiscountStrategy { // ... }这样调用方只需要一行代码DiscountStrategy strategy strategyContainer.getStrategy(order.getMemberLevel()); double discount strategy.calculate(order);新加一个会员等级只需要新增一个带名字注解的策略类既不用改 if-else也不用改容器完全符合开闭原则。我做过好几个项目都是用这个方式把原来几百行的折扣计算逻辑彻底拆干净的。当初大家还担心“拆这么多类会不会不好维护”实际用下来每个策略类都只有几行单独测试特别轻松反而比一坨大方法好维护得多。提示Spring 注入 MapString, T 时key 默认是 Bean 的名字。利用这个机制做策略分发是业务项目里最常见、也最省代码的玩法。4.4 策略模式不是万能药策略模式好用但有个边界问题不是所有 if-else 都应该用策略模式干掉。如果分支只有两三个而且每个分支的逻辑就一两行硬拆策略类反而是过度设计。一个策略类最少也要十几二十行代码两个分支就拆两个类项目里全是小类阅读成本并不比 if-else 低。我自己的判断标准是分支数量在三个以上或者每个分支的逻辑超过十行或者这个分支逻辑将来大概率会扩展。三者至少占两个才值得用策略模式。另外如果业务本身就是数据驱动的简单映射比如根据状态码返回提示文案用 Map 直接查就够了不需要建策略类。记住模式是给“会变”的代码准备的不是给所有代码准备的。还有一点策略模式解决的是“算法选择”问题它不负责“对象创建”。很多初学者容易把策略模式和工厂模式搞混。我教你一个记忆方法工厂模式回答的是“这个对象是怎么造出来的”策略模式回答的是“这个算法我选哪个来跑”。工厂造出来的对象也可以交给策略模式去选择使用两者经常一起出现。5. 观察者模式让代码学会广播而不是到处打电话5.1 没有观察者时的“打电话”写法观察者模式解决的是“一件事发生了要通知一堆人”的耦合问题。最典型的例子是下单成功。订单创建成功以后要做的事情可多了扣库存、发短信、送积分、记日志、推物流信息。刚接手项目的同学很容易直接在下单方法里一行一行往下写public void createOrder(Order order) { // 1. 保存订单 orderDao.save(order); // 2. 扣库存 stockService.deduct(order); // 3. 发短信 smsService.send(order.getUser().getPhone(), 下单成功); // 4. 送积分 pointService.add(order.getUser().getId(), 100); // 5. 记录日志 logService.record(order); }这种写法的矛盾在项目初期不明显后期会越来越痛。每加一个“下单后要做的事”就要跑到 createOrder 方法里加一行如果某个通知逻辑要去掉又得回来改而且下单这个方法越来越长测试也越来越难写。更烦的是这些通知逻辑经常由不同的小组维护大家都在往同一个方法里加代码冲突是必然的。这种一个方法里依次调用一堆后置处理的写法本质上就是“打电话”发布方要知道每一个接收方的电话号码。5.2 手写一个观察者模式观察者模式的思路很简单下单这件事是“被观察的”通知逻辑是“观察者”。订单创建后只需要发布一个“事件”所有关心这个事件的观察者自己会跳出来处理互不打扰。先定义观察者接口和事件类public interface OrderObserver { void onOrderCreated(Order order); } public class OrderCreatedEvent { private final Order order; public OrderCreatedEvent(Order order) { this.order order; } public Order getOrder() { return order; } }然后是主题被观察者它维护一个观察者列表并负责通知public class OrderPublisher { private final ListOrderObserver observers new ArrayList(); public void register(OrderObserver observer) { observers.add(observer); } public void unregister(OrderObserver observer) { observers.remove(observer); } public void publish(OrderCreatedEvent event) { for (OrderObserver observer : observers) { observer.onOrderCreated(event.getOrder()); } } }各个通知逻辑实现观察者接口public class StockObserver implements OrderObserver { Override public void onOrderCreated(Order order) { System.out.println(扣减库存: order.getId()); } } public class SmsObserver implements OrderObserver { Override public void onOrderCreated(Order order) { System.out.println(发送短信给用户: order.getUser().getPhone()); } }这时候业务代码里的下单逻辑变成了public void createOrder(Order order) { orderDao.save(order); publisher.publish(new OrderCreatedEvent(order)); }以后要加“下单后赠送优惠券”只需要新增一个 CouponObserver在启动时注册进去createOrder 一行都不用改。这就是观察者模式最核心的价值发布方和订阅方彻底解耦。5.3 观察者模式在 Spring 里的优雅实现手写观察者模式是理解原理的好办法但真实项目里我们通常直接用 Spring 的事件机制连上面的 publisher 都不用自己写。Spring 提供了 ApplicationEventPublisher发布方注入它然后调用 publishEvent 发布事件Service public class OrderService { private final ApplicationEventPublisher publisher; public OrderService(ApplicationEventPublisher publisher) { this.publisher publisher; } public void createOrder(Order order) { orderDao.save(order); publisher.publishEvent(new OrderCreatedEvent(this, order)); } }事件类可以继承 ApplicationEvent不过新版 Spring 里其实不强制了直接发布一个普通对象也可以public class OrderCreatedEvent { private final Order order; public OrderCreatedEvent(Order order) { this.order order; } public Order getOrder() { return order; } }监听方用 EventListener 注解方法签名直接匹配事件类型Component public class SmsListener { EventListener public void onOrderCreated(OrderCreatedEvent event) { Order order event.getOrder(); System.out.println(发送短信: order.getUser().getPhone()); } }Spring 的这个机制比手写的观察者模式好用太多你不用自己维护观察者列表不需要注册和注销容器全管了。加一个监听器就是加一个 Component 的事。5.4 观察者模式的三个坑观察者模式也有不少坑我按踩过的顺序讲三个。第一同步通知会拖慢主流程。手写观察者模式里publisher 的循环通知是同步的如果某个观察者处理特别慢比如发短信超时下单接口的响应时间会被拖住。Spring 里可以用 Async 把监听方法异步化但要注意异步方法拿不到原始线程的上下文比如事务、ThreadLocal 里的信息都可能丢失用之前要想清楚。第二事务边界问题。下单方法通常在一个事务里而 Spring 默认的 EventListener 是在事务提交前执行的。如果监听器里查数据库可能会查到还没提交的数据。想要事件在事务提交后再处理要用 TransactionalEventListener并指定 phase 为 AFTER_COMMIT。第三观察者注册后忘记注销会造成内存泄漏。手写版本里如果观察者对象没有被移除而它又持有大对象引用那这个对象永远无法被回收。我在一个桌面应用里吃过这个亏注册了很多监听器界面开关几次内存就往上涨。用 Spring 容器管理监听器会有帮助但手写场景下一定要养成配对注册和注销的习惯。注意观察者模式适合“被通知方不需要关心发布方结果”的场景。如果发布后你必须确认每个观察者都处理成功一个都不能失败那观察者模式就不合适不如直接按顺序调用或者在事件里自己做补偿逻辑。6. 从四个模式到实战思维6.1 模式是词汇不是军规学到这里你会发现这四个模式其实都在强调同一件事把变的部分和不变的部分分开。单例管的是“实例唯一”工厂管的是“创建集中”策略管的是“算法可换”观察者管的是“通知解耦”。它们的本质都是应对变化。我见过一些团队把设计模式当成代码评审的硬指标动不动就要求“这里必须用某某模式”这其实走偏了。模式是解决问题的工具不是彰显水平的装饰品。一段简单直白的代码如果功能稳定、改动不频繁你非要用策略模式把它拆成五个类那不是在优化而是在给后来人增加阅读负担。真正的高手不是把所有模式都用上而是在合适的时机使用最合适的那一个。6.2 模式经常组合出现真实项目里模式很少单打独斗。工厂可以和策略组合工厂负责创建策略对象策略负责执行算法。比如上面的折扣场景可以先通过工厂创建一个对应会员等级的策略再把策略传给 Context 执行。观察者也可以和模板方法组合模板方法定义流程骨架观察者负责在某个步骤完成后广播事件让扩展点更加灵活。这些组合没有标准答案唯一的评判标准是改需求的时候你花的时间是不是越来越少。如果答案是肯定的那模式就用对了。如果答案是“改一个需求反而要动更多地方”那不管用了多少个模式方向都是错的。6.3 推荐的学习路径如果你想把这些模式真正变成自己的东西我的建议是三条路并行。第一条读源码。Spring、MyBatis、JDK 的并发包里全是模式的影子比如 JDK 里 Runtime.getRuntime() 就是单例线程池的设计里就有工厂思想。读源码不是要你把每行都看懂而是去找你学过的模式的“身影”理解它们在实际代码里是怎么落地的。第二条重构旧代码。找一段你自己写的、现在看着头疼的代码尝试用这四个模式里的某一个去重构。改完跑一遍测试看看改动范围是不是变小了。这个练习比刷十道八股文有用得多。第三条写测试。模式的很多价值其实要通过测试才能真正感受到。策略模式拆开以后每个策略类都可以单独写测试再也不需要构造几十种组合去覆盖一个大方法的全部分支。测试好写往往意味着代码结构已经变好了。我个人在实际项目里体会最深的一点是设计模式的引入要趁早但不能强行。趁早是说在功能还在快速增长期就提前把扩展点留好不强行是说永远不要因为“别人用了所以我也要用”而去套模式。代码是给人读的也是要拿来改的一个模式如果不能让你的代码更好改那它就只是负担。最后再分享一个小技巧当你纠结某个地方要不要用模式时先用最朴素的写法把它实现出来同时在心里记一笔“这里将来可能会变”。等变化真的来了你自然会知道该往哪个方向重构。我踩过几次坑之后发现比起一开始就设计一套复杂的模式方案等需求落地后再做一次有目的的重构反而更稳也更贴近真实业务。