设计模式题最怕两种回答:一种是背定义,另一种是把所有模式都往项目里塞。好的回答会说明「这个变化点在哪里、引入模式后谁不需要改」。
1. SOLID 原则怎么落到代码里?
单一职责:一个类只对一类变化负责。判断标准不是「代码行数」,而是「改一个需求时,是否总在改同一个类」。
开闭原则:对扩展开放、对修改关闭。实践上靠接口、策略注册和依赖注入,而不是靠预测所有未来需求。
里氏替换:子类必须能在不破坏语义的前提下替换父类。典型反例是正方形继承长方形、或子类重写方法后抛出父类没有的异常。
接口隔离:接口要小而专,避免实现类被迫实现用不到的方法。依赖倒置:高层模块依赖抽象,Spring 的注入就是它的落地。
此外还有迪米特法则(少暴露内部结构)、组合优于继承。原则之间会冲突,真正的能力是在具体场景里取舍。
2. 单例有几种写法?哪种最好?
饿汉式在类加载时就创建,简单且线程安全,但会提前占用资源;懒汉式延迟创建,朴素的加锁实现性能差。
双重检查锁需要 volatile 修饰实例,否则可能读到「引用已赋值但构造未完成」的对象。静态内部类利用类加载机制实现懒加载和线程安全,写法最简洁。
枚举单例天然防止反射和反序列化破坏,是《Effective Java》推荐的方式;如果不需要延迟加载,它是最稳的选择。
要注意:Spring 的单例是「容器内单例」,不是 JVM 全局单例;一个类可以被多个容器各创建一个实例。分布式场景下,单例也只在本进程内有效。
3. 工厂模式解决了什么问题?
简单工厂用一个方法按参数创建对象,把 new 集中起来;工厂方法把创建延迟到子类;抽象工厂用于创建一组相关产品。
它们的共同价值是:调用方只依赖抽象,不关心具体类型和构造细节;新增产品时尽量不改调用方。Spring 的 BeanFactory 就是工厂思想,配合配置和依赖注入,把对象创建与使用解耦。
但工厂不是越多越好。对象只有一个实现、构造也简单时,直接 new 更清楚。工厂真正有价值,是在创建逻辑复杂(需要选择实现、组装依赖、缓存实例)或需要统一生命周期时。
如果工厂里出现大量 if/else 分支,往往说明可以用注册表加策略来替代。
4. 策略模式怎么消除 if-else?边界在哪?
先抽象出策略接口,每个分支实现一个策略;再用 Map<String, Strategy> 注册,调用时按类型取策略。注册可以手写,也可以让 Spring 注入所有实现并按标记建索引。
/**
* 按支付渠道选择策略并执行支付。
*
* @param channel 支付渠道标识
* @param amount 支付金额,单位分
* @return 支付结果
*/
public PayResult pay(String channel, long amount) {
PayStrategy strategy = strategies.get(channel);
if (strategy == null) {
throw new IllegalArgumentException("不支持的渠道: " + channel);
}
return strategy.pay(amount);
}
边界在于:如果分支逻辑很短、变化不频繁,保留 switch 反而更易读。策略模式增加的是类数量和间接层,只有当每个分支都有独立复杂度、需要独立测试或扩展时才划算。
还要避免「策略只包一层空壳」:如果所有实现都在调同一个方法,只是参数不同,可能用配置更合适。
5. 模板方法和回调有什么区别?
模板方法在父类定义算法骨架,把可变步骤留给子类实现,属于继承式扩展。JdbcTemplate、AbstractList、Servlet 的 service 都是这个思路。
回调把一段行为作为参数传入,由框架在合适时机调用,属于组合式扩展。Java 里用函数式接口、匿名类或 Lambda 表达,比如 JdbcTemplate.query 的 RowMapper。
区别在耦合方式:模板方法要求继承,父类改动会影响所有子类;回调更灵活,可以复用同一个方法,也便于测试。
选择时优先组合:如果变化点只有一个或少数几个,回调足够;如果骨架固定且步骤较多,模板方法更清晰。Spring 的很多「模板」类其实同时提供了回调和子类扩展两种方式。
6. 代理、装饰、适配器怎么区分?
三者都通过包装对象改变行为,但目的不同。
代理控制访问:调用方和被代理对象实现同一接口,代理在调用前后加逻辑(鉴权、事务、远程调用)。Spring AOP 就是代理。
装饰增强功能:同样实现同一接口,但重点是叠加职责,可以层层嵌套,比如 BufferedInputStream 包 FileInputStream。
适配器转换接口:把一个接口转成调用方期望的另一个接口,解决不兼容问题,比如 InputStreamReader 把字节流转成字符流。
判断方法:看包装后接口是否变化、调用方是否需要感知。接口不变且为了控制访问是代理,为了增强是装饰,接口变了是适配器。
7. 观察者模式在 Spring 里怎么用?
Spring 的事件机制就是观察者模式:ApplicationEventPublisher 发布事件,@EventListener 标注的方法接收事件,容器负责找到监听器。
默认是同步调用,发布者会等所有监听器执行完;可以用 @Async 改异步,但要配置线程池,并注意异常不会传回发布者。@TransactionalEventListener 可以把监听时机绑定到事务提交后,避免「事务还没提交,监听器已经查库」。
风险包括:监听器抛异常影响主流程(同步时)、异步丢失顺序、循环发布导致死循环、监听器过多难以追踪。事件适合「一对多、弱依赖」的通知,不适合需要强一致返回结果的流程。
8. 责任链模式适合什么场景?
责任链把请求沿处理器链传递,每个处理器决定处理、继续还是中断。Servlet 的 Filter、Spring MVC 的 Interceptor、Netty 的 Pipeline 都是它的实现。
它的价值是解耦:新增一个校验或日志处理器,不需要改调用方;顺序和组合可以通过配置调整。
代价是链路不直观,出问题时需要知道「谁处理了、谁短路了」。要明确每个处理器的职责和顺序,并提供日志或追踪。
不要为了「看起来灵活」把简单流程拆成一堆处理器。两三个步骤的校验,用顺序代码更清楚;只有当处理器数量会增长、顺序需要动态调整时,责任链才值得。
易错点与自测
- 设计原则是权衡工具,不是越抽象越好;模式引入的间接层要有明确收益。
- 单例的线程安全取决于是否有可变状态,容器单例不等于全局单例。
- Spring 事件默认同步,异常处理和事务边界要显式设计。
自测:① 用策略模式改写一段三层的 if/else,并说明什么时候不该改;② 区分代理和装饰的意图差异;③ 解释 @TransactionalEventListener 解决什么问题。
参考资料
- Spring Framework:事件与监听器 — 同步、异步与事务事件
- Spring Framework:AOP 与代理 — 代理模式的实际应用
- Refactoring.Guru:设计模式目录 — 各模式的意图与结构对照