1. 为什么设计模式值得反复咀嚼
很多人第一次接触设计模式,是在培训班或者自学路线的某个章节里,翻到“第七章 设计模式”这种标题,心里第一反应往往是:又是背概念。工厂、单例、观察者、策略……名字听着都挺唬人,真到写业务代码的时候,好像一个也用不上。我刚开始写代码那几年也是这个心态,觉得这些是面试八股,是架构师才需要关心的东西,跟每天写增删改查的自己没什么关系。
后来踩的坑多了,才慢慢回过味来。设计模式根本不是让你去背的,它是一群人在大量真实项目里反复撞墙之后,总结出来的“避免重复踩坑的套路”。你写一个订单系统,状态流转越写越乱,if-else 堆到几百行,改一个逻辑要动五个文件——这时候你才会真正理解状态模式和策略模式到底在解决什么问题。你做一个消息推送模块,加一个渠道就要改一遍核心代码,改到最后没人敢动——这时候你才会明白观察者模式和发布订阅的价值。
所以这一章我打算换个讲法。不按教科书那种“定义、结构、优缺点、代码示例”的流水线来,而是从实际开发中遇到的痛点出发,讲清楚每个模式到底在什么场景下能救命,怎么落地,以及我自己踩过的那些坑。适合已经写过一些项目、但对设计模式还停留在“知道名字”阶段的开发者,也适合想重新梳理一遍知识体系的老手。核心关键词就几个:设计模式、代码复用、解耦、可维护性、重构。这些词不是拿来装门面的,是真正决定你的代码能不能活过三个版本迭代的东西。
2. 设计模式的整体思路与选型逻辑
2.1 设计模式到底解决的是什么问题
先把话说透。设计模式解决的核心问题只有一个:变化。需求会变,业务会变,技术栈会变,唯一不变的就是变化本身。如果一段代码永远不需要修改,那它写成什么样都无所谓,哪怕全部塞在一个函数里也没人管你。但现实是,代码是要被反复修改的,而设计模式就是让“修改”这件事变得可控、可预测、不容易引发连锁反应。
我习惯把设计模式按“变化维度”来分类理解,而不是按教科书里的创建型、结构型、行为型来死记。创建型模式(单例、工厂、建造者等)解决的是“对象怎么来”的问题,当对象的创建过程变得复杂、或者创建逻辑可能变化时,就需要它们。结构型模式(适配器、装饰器、代理等)解决的是“对象怎么组合”的问题,当类与类之间的关系变得错综复杂时,它们能帮你理清层次。行为型模式(策略、观察者、状态、责任链等)解决的是“对象怎么协作”的问题,当多个对象之间的交互逻辑变得难以维护时,它们就派上用场了。
这个分类方式的好处是,你在实际开发中遇到问题时,可以先判断“变化发生在哪个环节”,然后直接去对应的类别里找工具,而不是把二十几个模式从头到尾想一遍。
2.2 什么时候该用,什么时候不该用
这是我最想强调的一点:设计模式不是越多越好,滥用比不用更可怕。我见过一个项目,总共不到五千行代码,用了十几种设计模式,结果新人接手的时候看了一周都没理清调用链路。每个类都很“优雅”,但整体复杂度爆炸,改一个功能要跳十几个文件。这就是典型的过度设计。
我的判断标准很简单,就三条:
- 有明确的变化点:如果某个逻辑未来大概率会变(比如支付渠道、消息推送方式、数据源切换),那就值得用模式把它抽象出来。如果它几乎不可能变,直接写死反而更清晰。
- 有重复的痛苦:同样的代码结构在项目里出现了三次以上,每次加新功能都要复制粘贴再改一遍,这时候就该考虑抽象了。
- 有维护的困惑:团队里其他人看你的代码,经常问“这个逻辑到底在哪”“改这里会不会影响那边”,说明耦合太严重,需要解耦。
反过来,如果只是“觉得这样写比较高级”“面试可能会问”,那还是老老实实写简单直接的代码。代码的第一优先级永远是让人看懂,而不是让人惊叹。
2.3 从痛点出发的模式选型表
为了让你有个直观的参照,我整理了一张常见痛点与对应模式的速查表。这张表不是让你背的,是让你在遇到具体问题时能快速定位到可能的解法。
| 实际痛点 | 推荐模式 | 核心思路 |
|---|---|---|
| 全局只需要一个实例,多处访问要一致 | 单例模式 | 控制实例数量,提供统一访问点 |
| 对象创建逻辑复杂,或需要根据条件创建不同对象 | 工厂模式 | 把创建过程封装起来,调用方只关心接口 |
| 一个对象状态变化,多个对象需要同步响应 | 观察者模式 | 建立一对多依赖,状态变更自动通知 |
| 同一行为有多种算法实现,需要动态切换 | 策略模式 | 把算法独立封装,运行时可替换 |
| 对象行为随内部状态改变而改变 | 状态模式 | 把状态抽象成独立类,消除大量条件判断 |
| 请求需要经过多个处理环节,且环节可增减 | 责任链模式 | 把处理者串成链,请求沿链传递 |
| 需要在不修改原类的前提下扩展功能 | 装饰器模式 | 通过包装层层叠加能力 |
| 新旧接口不兼容,需要桥接 | 适配器模式 | 转换接口,让不兼容的类能协作 |
这张表我建议你收藏,下次写代码卡壳的时候翻出来看一眼,比翻书快得多。
3. 核心模式深度拆解与实操要点
3.1 单例模式:看似简单,坑最多
单例模式大概是所有设计模式里最“简单”的一个,但也是实际项目中出问题最多的一个。它的核心诉求很明确:一个类在全局只有一个实例,并提供统一的访问入口。典型场景包括配置管理器、连接池、日志记录器、缓存管理器等。
先看一个最基础的实现,以 Java 为例:
public class ConfigManager { private static ConfigManager instance; private Map<String, String> configs; private ConfigManager() { configs = new HashMap<>(); // 模拟加载配置 configs.put("timeout", "3000"); configs.put("retry", "3"); } public static synchronized ConfigManager getInstance() { if (instance == null) { instance = new ConfigManager(); } return instance; } public String get(String key) { return configs.get(key); } }这段代码能跑,但有几个问题需要说清楚。第一,synchronized加在方法上,每次调用getInstance()都要抢锁,性能在高并发场景下会有明显损耗。第二,如果构造函数里做了耗时操作(比如读文件、建连接),第一次调用会阻塞其他线程。第三,也是最容易被忽略的——反射可以破坏单例,通过反射调用私有构造函数就能创建新实例。
改进方案是用双重检查锁定(Double-Checked Locking):
public class ConfigManager { private static volatile ConfigManager instance; private Map<String, String> configs; private ConfigManager() { if (instance != null) { throw new IllegalStateException("单例已被创建,禁止反射调用"); } configs = new HashMap<>(); configs.put("timeout", "3000"); } public static ConfigManager getInstance() { if (instance == null) { synchronized (ConfigManager.class) { if (instance == null) { instance = new ConfigManager(); } } } return instance; } }注意volatile关键字不能省。它保证的是可见性和有序性,防止指令重排序导致其他线程拿到一个“半初始化”的对象。这个坑我在一个线上项目里真实遇到过:配置对象还没完全初始化就被另一个线程读取,导致超时参数读出来是 null,排查了大半天才定位到。
实操心得:如果你的项目用 Spring 框架,其实大部分单例场景已经被 Spring 容器接管了。Spring 的 Bean 默认就是单例的,你不需要自己再写单例模式。自己手写单例的场合,通常是那些不归容器管理的工具类或者底层组件。
还有一个常见误区:单例不等于线程安全。单例只保证实例唯一,但实例内部的成员变量如果被多线程同时读写,照样会出问题。我见过有人在单例里放了一个SimpleDateFormat对象,结果多线程格式化时间的时候各种报错。解决办法是把可变状态改成局部变量,或者用ThreadLocal隔离。
3.2 工厂模式:把“创建”这件事管起来
工厂模式的核心价值在于解耦对象的创建和使用。调用方不需要知道对象是怎么造出来的,只需要告诉工厂“我要什么”,工厂返回对应的实例。这在对象创建逻辑复杂、或者需要根据配置动态决定创建哪种对象时特别有用。
举个实际例子。假设你在做一个支付模块,支持多种支付渠道。不用工厂模式的话,代码大概长这样:
public class PaymentService { public void pay(String channel, BigDecimal amount) { if ("alipay".equals(channel)) { AlipayClient client = new AlipayClient(); client.setAppId("xxx"); client.setPrivateKey("xxx"); client.pay(amount); } else if ("wechat".equals(channel)) { WechatPayClient client = new WechatPayClient(); client.setMchId("xxx"); client.setApiKey("xxx"); client.pay(amount); } else if ("unionpay".equals(channel)) { // 又一堆初始化代码 } // 每加一个渠道就要改这里 } }这种代码的问题很明显:每加一个支付渠道,就要修改PaymentService,违反开闭原则;初始化逻辑散落在业务代码里,重复且容易出错;测试的时候没法替换成 mock 实现。
用工厂模式重构之后:
public interface PaymentChannel { void pay(BigDecimal amount); } public class AlipayChannel implements PaymentChannel { private AlipayClient client; public AlipayChannel() { client = new AlipayClient(); client.setAppId(Config.get("alipay.appId")); client.setPrivateKey(Config.get("alipay.privateKey")); } @Override public void pay(BigDecimal amount) { client.pay(amount); } } public class PaymentChannelFactory { private static final Map<String, Supplier<PaymentChannel>> registry = new HashMap<>(); static { registry.put("alipay", AlipayChannel::new); registry.put("wechat", WechatChannel::new); registry.put("unionpay", UnionPayChannel::new); } public static PaymentChannel create(String channel) { Supplier<PaymentChannel> supplier = registry.get(channel); if (supplier == null) { throw new IllegalArgumentException("不支持的支付渠道: " + channel); } return supplier.get(); } }重构之后,PaymentService只需要调用PaymentChannelFactory.create(channel).pay(amount),新增渠道时只需要写一个新的实现类并注册到工厂里,完全不用动业务代码。这就是工厂模式的核心价值:把变化关进笼子里。
注意事项:工厂类本身不要变成“上帝类”。我见过一个工厂类注册了几十个产品,文件上千行,维护起来同样痛苦。如果产品数量太多,可以考虑按业务域拆分成多个工厂,或者用注解+反射的方式自动注册。
3.3 观察者模式:事件驱动架构的基石
观察者模式解决的是一对多的依赖关系:当一个对象的状态发生变化时,所有依赖它的对象都能自动收到通知。这个模式在前端开发里几乎是标配(事件监听),在后端也广泛用于消息通知、日志记录、缓存刷新等场景。
我拿一个订单创建后的处理流程来举例。订单创建成功后,可能需要做很多事情:发短信通知用户、给运营发邮件、更新库存、写日志、刷新缓存。如果全部塞在订单服务里:
public void createOrder(Order order) { orderDao.save(order); smsService.send(order.getUserId(), "订单创建成功"); emailService.sendToOps(order); inventoryService.deduct(order); logService.log(order); cacheService.refresh(order.getUserId()); // 后面还要加什么? }这种写法的问题在于:订单服务和所有下游服务强耦合,任何一个下游出问题都会影响主流程,新增一个处理逻辑就要改订单服务。用观察者模式重构:
public interface OrderEventListener { void onOrderCreated(Order order); } public class OrderEventPublisher { private final List<OrderEventListener> listeners = new CopyOnWriteArrayList<>(); public void register(OrderEventListener listener) { listeners.add(listener); } public void publishOrderCreated(Order order) { for (OrderEventListener listener : listeners) { try { listener.onOrderCreated(order); } catch (Exception e) { // 单个监听器异常不影响其他监听器 log.error("监听器执行失败", e); } } } }订单服务只需要调用publisher.publishOrderCreated(order),至于谁来处理、怎么处理,它完全不关心。新增处理逻辑时,写一个新的监听器注册进去就行。
实操心得:观察者模式最大的坑是异常处理。如果某个监听器抛异常导致后续监听器不执行,会引发很难排查的问题。我的做法是每个监听器单独 try-catch,并且记录详细日志。另外,如果监听器执行很慢,会拖累主流程,这时候要考虑异步化——把通知丢到线程池或者消息队列里处理。
还有一个容易被忽略的点:监听器的执行顺序。如果多个监听器之间有依赖关系(比如必须先扣库存再发短信),那用简单的 List 存储就不够了,需要引入优先级或者改成责任链模式。这个我在后面讲责任链的时候会展开。
3.4 策略模式:消灭 if-else 的利器
策略模式是我在日常开发中用得最多的模式,没有之一。它的核心思想是:把一组算法分别封装起来,让它们可以互相替换。调用方持有的是算法接口,具体用哪个实现可以在运行时决定。
最典型的场景就是各种“根据类型走不同逻辑”的需求。比如运费计算:普通用户按重量算、VIP 用户包邮、偏远地区加收附加费。不用策略模式的写法:
public BigDecimal calculateFreight(User user, Order order) { if (user.isVip()) { return BigDecimal.ZERO; } else if (order.isRemoteArea()) { return baseFreight(order).add(remoteSurcharge(order)); } else { return baseFreight(order); } }现在需求变了:VIP 也分等级,不同等级包邮门槛不同;偏远地区定义要动态配置;还要支持促销期间的免运费活动。这时候 if-else 就会膨胀到无法维护。
用策略模式重构:
public interface FreightStrategy { BigDecimal calculate(Order order); boolean support(User user, Order order); } public class VipFreightStrategy implements FreightStrategy { @Override public BigDecimal calculate(Order order) { return BigDecimal.ZERO; } @Override public boolean support(User user, Order order) { return user.isVip(); } } public class RemoteAreaFreightStrategy implements FreightStrategy { @Override public BigDecimal calculate(Order order) { return baseFreight(order).add(remoteSurcharge(order)); } @Override public boolean support(User user, Order order) { return order.isRemoteArea(); } } public class FreightCalculator { private final List<FreightStrategy> strategies; public FreightCalculator(List<FreightStrategy> strategies) { this.strategies = strategies; } public BigDecimal calculate(User user, Order order) { return strategies.stream() .filter(s -> s.support(user, order)) .findFirst() .orElseThrow(() -> new IllegalStateException("没有匹配的运费策略")) .calculate(order); } }这种写法的好处是:每个策略独立可测试,新增策略不用改现有代码,策略的匹配条件也可以灵活调整。我特别喜欢support方法这个设计,它让策略自己决定“我适不适合处理这个请求”,而不是让调用方去判断。
注意事项:策略模式不要滥用。如果只有两三个分支且几乎不会变,直接 if-else 反而更清晰。我见过有人把简单的“是否为空”判断也封装成策略,纯属给自己找麻烦。判断标准还是那三条:有变化点、有重复痛苦、有维护困惑。
3.5 状态模式:让状态流转不再混乱
状态模式和策略模式在结构上很像,但解决的问题不同。策略模式关注的是“选哪个算法”,状态模式关注的是“当前处于什么状态,能做什么,做完之后转到什么状态”。订单系统、工作流引擎、游戏角色状态机,都是状态模式的典型应用场景。
不用状态模式的订单状态管理,代码大概是这样:
public void handleOrder(Order order, String action) { if ("UNPAID".equals(order.getStatus())) { if ("pay".equals(action)) { order.setStatus("PAID"); } else if ("cancel".equals(action)) { order.setStatus("CANCELLED"); } } else if ("PAID".equals(order.getStatus())) { if ("ship".equals(action)) { order.setStatus("SHIPPED"); } else if ("refund".equals(action)) { order.setStatus("REFUNDING"); } } else if ("SHIPPED".equals(order.getStatus())) { // 又是一堆判断 } // 状态越多,嵌套越深 }这种代码的问题在于:状态和动作的组合散落在各处,新增一个状态要改所有相关的地方,而且很容易漏掉某些非法流转(比如已发货的订单不应该能取消)。
用状态模式重构:
public interface OrderState { void pay(OrderContext context); void ship(OrderContext context); void cancel(OrderContext context); String getName(); } public class UnpaidState implements OrderState { @Override public void pay(OrderContext context) { // 支付逻辑 context.setState(new PaidState()); } @Override public void ship(OrderContext context) { throw new IllegalStateException("未支付订单不能发货"); } @Override public void cancel(OrderContext context) { context.setState(new CancelledState()); } @Override public String getName() { return "UNPAID"; } }每个状态类只关心自己状态下能做什么、做完转到哪个状态。非法操作直接抛异常,逻辑清晰,新增状态只需要加一个类。
实操心得:状态模式在状态数量少的时候(三四个以内)可能显得有点重,但一旦状态超过五个,或者状态流转规则经常调整,它的优势就非常明显了。我在一个审批流项目里用状态模式重构了原本两千多行的状态判断代码,重构后每个状态类平均不到一百行,新人接手半天就能理清整个流程。
4. 实操过程与核心环节实现
4.1 从零搭建一个可扩展的通知系统
光讲理论没意思,我带你完整走一遍实际项目的搭建过程。需求是这样的:一个系统需要支持多种通知方式(短信、邮件、站内信、推送),通知的触发时机和内容模板可以配置,后续要能方便地新增通知渠道。
第一步:定义通知渠道接口
public interface NotificationChannel { void send(NotificationMessage message); String getChannelName(); }第二步:实现各个渠道
public class SmsChannel implements NotificationChannel { private final SmsClient client; public SmsChannel(SmsClient client) { this.client = client; } @Override public void send(NotificationMessage message) { client.send(message.getPhone(), message.getContent()); } @Override public String getChannelName() { return "sms"; } }邮件、站内信、推送渠道类似,各自封装自己的发送逻辑。
第三步:用工厂管理渠道创建
public class NotificationChannelFactory { private final Map<String, NotificationChannel> channels = new ConcurrentHashMap<>(); public void register(NotificationChannel channel) { channels.put(channel.getChannelName(), channel); } public NotificationChannel get(String name) { NotificationChannel channel = channels.get(name); if (channel == null) { throw new IllegalArgumentException("未知通知渠道: " + name); } return channel; } }第四步:用观察者模式解耦触发逻辑
public class NotificationEventPublisher { private final List<NotificationEventListener> listeners = new CopyOnWriteArrayList<>(); public void register(NotificationEventListener listener) { listeners.add(listener); } public void publish(NotificationEvent event) { for (NotificationEventListener listener : listeners) { try { listener.onEvent(event); } catch (Exception e) { log.error("通知事件处理失败: {}", listener.getClass().getSimpleName(), e); } } } }第五步:用策略模式决定用哪个渠道
public class NotificationDispatcher { private final NotificationChannelFactory factory; private final Map<String, NotificationStrategy> strategies; public void dispatch(NotificationMessage message) { String channelName = strategies.get(message.getType()) .selectChannel(message); factory.get(channelName).send(message); } }整个系统搭下来,新增一个通知渠道只需要三步:写一个实现类、注册到工厂、配置好策略。完全不用动核心逻辑。这就是设计模式组合使用的威力——每个模式解决一个维度的问题,组合起来形成一套可扩展的架构。
4.2 参数选择与配置的量化考量
在实际落地过程中,有几个参数和配置需要仔细考量,不能拍脑袋决定。
线程池大小:如果通知发送是异步的,线程池大小怎么定?我的经验公式是:核心线程数 = CPU核数 * 2,最大线程数 = CPU核数 * 4,队列容量根据峰值流量估算。比如峰值每秒 1000 条通知,平均处理耗时 50ms,那么需要的并发线程数大约是1000 * 0.05 = 50个。但这是理论值,实际还要考虑下游接口的承受能力,不能把下游打挂。
重试次数与退避策略:通知发送失败是常态,重试策略很关键。我一般配置三次重试,间隔采用指数退避:第一次 1 秒,第二次 5 秒,第三次 30 秒。这样既能应对临时故障,又不会在对方服务彻底挂掉时疯狂重试造成雪崩。
超时时间:每个渠道的超时时间要单独配置。短信接口一般 3 秒,邮件接口可以放宽到 10 秒,推送接口 5 秒。超时时间设置的原则是:宁可快速失败,也不要让请求堆积。
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| 核心线程数 | CPU核数 * 2 | 根据实际压测调整 |
| 最大线程数 | CPU核数 * 4 | 防止无限扩张 |
| 队列容量 | 1000-5000 | 根据内存和峰值估算 |
| 重试次数 | 3 | 超过三次基本是系统性问题 |
| 首次重试间隔 | 1秒 | 给下游喘息时间 |
| 超时时间 | 3-10秒 | 按渠道特性配置 |
实操心得:这些参数没有标准答案,一定要结合压测结果来调。我见过一个项目直接抄了网上的配置,结果线程池队列设成了无界队列,高峰期内存直接爆掉。任何配置都要有上限,无界队列是定时炸弹。
4.3 重构遗留代码的实操记录
我接手过一个老项目,订单状态管理代码有三千多行,全部是 if-else 嵌套,最深的地方有七层。每次加新状态都要花两天时间,还经常出 bug。我用状态模式重构的过程大致是这样的:
第一阶段:梳理状态和流转规则。把所有的状态列出来,把每个状态下允许的操作和流转目标整理成一张表。这一步最关键,很多隐藏的非法流转就是在这时候发现的。
第二阶段:定义状态接口和上下文。把状态相关的操作抽象成接口方法,上下文持有当前状态对象。
第三阶段:逐个迁移状态。不要一次性全改,而是每次迁移一个状态,跑通测试再迁移下一个。这样风险可控,出问题也容易定位。
第四阶段:清理旧代码。所有状态迁移完成后,删除原来的 if-else 代码,补充单元测试。
整个重构花了一周时间,重构后代码从三千行降到八百行,新增一个状态的时间从两天缩短到两小时。这个投入产出比是非常划算的。
5. 常见问题与排查技巧实录
5.1 设计模式使用中的典型翻车现场
问题一:单例被反射破坏。前面提过,反射可以调用私有构造函数创建新实例。解决办法是在构造函数里加判断,如果实例已存在就抛异常。更彻底的方式是用枚举实现单例,枚举天然防止反射和序列化破坏。
问题二:工厂类无限膨胀。工厂类注册的产品越来越多,最后变成一个巨大的 Map。解决办法是按业务域拆分工厂,或者用注解+类路径扫描自动注册。
问题三:观察者模式导致内存泄漏。监听器注册后忘记注销,导致对象无法被回收。解决办法是提供unregister方法,并且在合适的生命周期节点(比如页面销毁、请求结束)主动注销。用弱引用(WeakReference)存储监听器也是一种方案,但要小心监听器被提前回收。
问题四:策略模式匹配不到策略。多个策略的support条件有重叠,或者都不匹配。解决办法是明确策略的优先级,并且在找不到策略时给出明确的错误信息,而不是静默返回 null。
问题五:状态模式状态爆炸。状态数量太多,每个状态类都要实现所有接口方法,大量方法是空的或者抛异常。解决办法是把接口拆细,或者用抽象基类提供默认实现。
5.2 问题排查速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 单例获取到不同实例 | 类加载器不同、反射调用、序列化 | 检查类加载器、构造函数防护、readResolve |
| 工厂创建对象失败 | 未注册、注册名拼写错误 | 打印注册表内容、检查配置 |
| 监听器不执行 | 未注册、异常被吞、异步线程池满 | 检查注册逻辑、日志、线程池状态 |
| 策略选择错误 | support 条件重叠、优先级未定义 | 打印所有策略的匹配结果 |
| 状态流转异常 | 非法操作未拦截、状态未正确更新 | 加状态变更日志、检查状态设置时机 |
| 装饰器顺序错误 | 包装顺序影响结果 | 明确装饰器执行顺序,加注释说明 |
5.3 独家避坑技巧
技巧一:给每个模式加日志。在工厂创建对象、策略选择、状态流转的关键节点打日志,出问题的时候能快速定位。日志级别用 DEBUG,生产环境可以动态开启。
技巧二:用单元测试锁定行为。每个策略、每个状态都写单元测试,确保重构不改变行为。这是重构遗留代码的安全网。
技巧三:不要一次性引入太多模式。一个模块最多用两到三个模式,多了就是过度设计。先用最简单的方式实现,等痛点真正出现了再重构。
技巧四:模式命名要贴合业务。不要叫ConcreteStrategyA、ConcreteStrategyB,要叫VipFreightStrategy、RemoteAreaFreightStrategy。名字本身就是最好的文档。
技巧五:画一张调用关系图。模式用多了之后,类之间的关系会变复杂。在文档里画一张简单的调用关系图,新人接手时能省很多沟通成本。
6. 设计模式的学习路径与进阶建议
6.1 从“知道”到“会用”的三个阶段
我观察过很多开发者的学习路径,大致分三个阶段。第一阶段是“知道名字”,能说出二十几种模式的定义,但写代码时想不起来用。第二阶段是“刻意使用”,遇到问题会主动想“这里能不能用某个模式”,但经常用错或者用过头。第三阶段是“自然流露”,写代码的时候不知不觉就用上了合适的模式,甚至不需要刻意去想它叫什么名字。
从第一阶段到第二阶段,靠的是刻意练习。我的建议是每学一个模式,就在自己的项目里找一个可以应用的场景,哪怕是很小的场景。比如把一段 if-else 改成策略模式,把一个全局变量改成单例。练得多了,模式就变成肌肉记忆了。
从第二阶段到第三阶段,靠的是复盘。每次项目上线后,回头看看哪些地方模式用得好,哪些地方用过头了。我自己的经验是,用过头的情况往往比不用的情况更多,所以复盘的时候重点看“哪些抽象是不必要的”。
6.2 不同规模项目的模式选择建议
小型项目(几千行代码):建议只用最基础的模式,比如单例、简单工厂、策略。不要引入太多抽象,快速迭代比架构优雅更重要。
中型项目(几万行代码):可以系统性地使用创建型和行为型模式。重点是解耦和可测试性,工厂、策略、观察者、状态这几个模式会经常用到。
大型项目(十万行以上):模式的使用要形成团队规范,避免每个人按自己的理解乱用。建议制定一份“模式使用指南”,明确什么场景用什么模式,以及命名规范。
6.3 后续可以深入的方向
如果你已经把常见模式用熟了,可以往这几个方向深入。一是源码阅读,JDK、Spring、Netty 这些框架里大量使用了设计模式,看它们怎么用比看教科书收获大得多。二是架构模式,设计模式是类级别的,架构模式是系统级别的,比如分层架构、事件驱动架构、微内核架构,这些是设计模式在更大尺度上的应用。三是函数式编程中的模式,很多经典模式在函数式语言里有更简洁的表达方式,比如策略模式可以用高阶函数替代,观察者模式可以用响应式流替代。
我个人在实际操作中的体会是,设计模式这东西,学的时候觉得多,用的时候觉得少,真正融会贯通之后又觉得它本来就应该这样。它不是额外的负担,而是帮你把代码写得更顺手的工具。关键还是那句话:从痛点出发,按需使用,不要为了模式而模式。