
先给出一个可能反常识的结论大部分人在面试里把设计模式背得滚瓜烂熟但真正写代码的时候能用对的场合一只手数得过来。我见过太多人拿着工厂模式往每一个类上套也见过有人为了省事把策略模式写成一堆if-else最后代码比不用模式的时候还难维护。设计模式不是装饰品它是用来解决问题的问题都没搞清楚就往上套结果只有一个——制造新问题。这篇文章我打算换个讲法不列23种模式的定义而是从最常见的几类问题场景出发把“为什么需要这个模式”“它到底解决什么问题”“在Java里怎么写最合适”说清楚。内容主要面向准备面试的Java开发也适合那些已经工作但觉得模式用不上、或者不知道什么时候该用模式的写代码的人。1. 先搞清楚设计模式解决的是变化问题不是优雅问题很多人把设计模式当成一种“高级写法”觉得代码写得抽象就是好代码。这是最大的误解。设计模式的核心目的只有一个——应对变化。代码里的变化点在哪里哪里才需要设计模式没有变化的地方硬上模式只会让代码变得又绕又难读。1.1 判断是否需要设计模式的一个简单标准我会问自己一个问题这段逻辑未来会不会有多种实现或者多种形态如果答案是“大概率不会”那直接写死就行。如果答案是“会”再考虑用什么模式去隔离变化。比如一个工具类里的加密方法公司目前只有AES一种要求那就老老实实写一个静态方法。什么时候需要抽象成接口当合规要求可能出现国密SM4、或者不同项目组要接不同加密方案的时候再抽象。提前抽象不是不行但你要支付额外的理解成本和维护成本。1.2 八股文和真会用的分界线在哪面试里经常有人把23种模式背得一字不差但问到“你这个项目里哪里用到了什么模式”就哑火。真正的分界线是你能不能在别人的代码或者框架源码里认出模式。Spring、MyBatis、Netty这些框架里全是设计模式你不需要自己硬造场景能看懂框架里的模式、能说清楚它为什么要这么设计就已经比大多数背定义的人强太多了。如果非要说个判断标准那就是设计模式的知识只有在“你能指着某段真实代码说出它用了什么模式、为什么用”的时候才算真正是你的能力。2. 创建对象这件事工厂模式家族以及90%的人是怎么用错的工厂模式是面试问得最多、也是在实际代码里被滥用得最惨的模式。创建型模式里有简单工厂、工厂方法、抽象工厂三个兄弟很多人压根分不清适用范围。2.1 简单工厂最容易被理解的入门款简单工厂不是一个正式的GoF模式但它是理解工厂思想的起点。思路很简单由一个工厂类根据参数决定创建哪一种产品对象。public class LoggerFactory { public static Logger createLogger(String type) { if (file.equals(type)) { return new FileLogger(); } else if (db.equals(type)) { return new DatabaseLogger(); } throw new IllegalArgumentException(Unknown logger type: type); } }调用方不需要知道FileLogger和DatabaseLogger的具体构造过程只需要传入一个type就拿到一个Logger。这个模式解决的是“多个同类对象的创建逻辑集中管理”的问题。但简单工厂有明显的天花板——每加一种新产品类型都要改工厂里的if-else违反了开闭原则。如果产品类型基本固定、短期不会扩展用简单工厂完全没问题一旦类型经常增加就得考虑更灵活的方案。2.2 工厂方法把创建这件事延迟到子类工厂方法把“要创建哪个产品”的决定权交给了子类。先定义一个抽象的工厂接口由不同的子类工厂去决定具体创建什么对象public interface LoggerFactory { Logger createLogger(); } public class FileLoggerFactory implements LoggerFactory { Override public Logger createLogger() { return new FileLogger(); } } public class DatabaseLoggerFactory implements LoggerFactory { Override public Logger createLogger() { return new DatabaseLogger(); } }这样当新增一种Logger时只需要新增一个产品类和一个对应的工厂类不需要改动已有代码符合开闭原则。代价是类的数量翻倍结构变复杂。实际项目中什么时候会用工厂方法一个很典型的场景是不同环境的配置初始化。比如开发环境和生产环境需要连接不同的中间件、使用不同的序列化组件用工厂方法可以让环境相关的初始化逻辑聚合在各自的工厂里。2.3 抽象工厂处理一族对象的创建抽象工厂解决的是“一系列相关产品”的创建问题。拿数据库连接举例一个应用可能同时用到Connection、Statement、ResultSet这三个对象之间是有配套关系的。抽象工厂能保证创建出来的始终是同一套产品族public interface DatabaseFactory { Connection createConnection(); Statement createStatement(); ResultSet createResultSet(); } public class MySqlFactory implements DatabaseFactory { // 创建MySQL系列的三个对象 } public class OracleFactory implements DatabaseFactory { // 创建Oracle系列的三个对象 }抽象工厂的典型应用在跨平台UI组件、跨数据库访问等场景。在你的日常业务代码里坦白讲用的机会不多但面试时可以体现你对“产品族”这个概念的理解。2.4 工厂模式在Spring里的存在感Spring的BeanFactory和ApplicationContext本质上就是一个大号工厂负责创建和管理对象。所以有一种说法是学了Spring之后你日常代码里基本不太需要自己写工厂——Spring已经把这件事做掉了。这也是为什么很多人觉得设计模式“用不上”——因为框架用掉了。但这里有个反直觉的点框架帮你做的往往是最通用的对象创建而你自己业务里那些“根据不同来源创建不同类型的处理器”的逻辑恰恰是最适合用工厂模式的地方。比如支付对接微信、支付宝、银联三个渠道接收回调之后的验签逻辑完全不同用一个PayProcessorFactory按渠道返回对应的处理器是再典型不过的工厂用法。3. 代理模式和装饰器模式两种包装心态别再傻傻分不清这两个模式代码结构长得很像都是持有一个被包装对象然后在外层做一些事情但它们的动机完全不同。3.1 代理模式控制访问而不是增强功能代理模式的出发点是“我不能直接碰这个对象或者不想直接碰所以我找个代理来替我操作”。典型场景懒加载对象创建开销大真正用到时才创建用代理延迟初始化。访问控制权限校验、防火墙检查通过代理拦截调用。远程调用RPC框架里本地调用的其实是一个代理对象真正干活的服务在另一台机器上。public class SecureImageProxy implements Image { private final String path; private RealImage realImage; private final User user; public SecureImageProxy(String path, User user) { this.path path; this.user user; } Override public void display() { if (!user.hasPermission(view_image)) { throw new SecurityException(无权限查看图片); } if (realImage null) { realImage new RealImage(path); } realImage.display(); } }注意看代理模式里的“代理”和“真实对象”通常实现同一个接口但代理并不在真实对象的基础上增加新功能它做的是控制、拦截、延迟这些“外围”的事。3.2 装饰器模式人畜无害地增强功能装饰器模式的出发点是“我要给这个对象动态加功能”而且加完之后返回的还是同一个接口类型调用方无感知。Java IO流是教科书级的装饰器使用现场BufferedReader reader new BufferedReader( new InputStreamReader( new FileInputStream(data.txt), StandardCharsets.UTF_8));FileInputStream负责读字节InputStreamReader负责把字节解成字符BufferedReader负责提供缓冲和按行读取功能。每一层都在“装饰”下一层但对外暴露的还是Reader接口。就算你没专门学过IO流的源码只要你用过BufferedReader就已经在享受装饰器模式的好处了——新功能是通过组合加进来的不用去改FileInputStream的代码。3.3 动态代理代理模式的高阶形态也是Spring AOP的地基静态代理要求每个被代理类都手写一个代理类太麻烦。动态代理的核心思路是在运行期动态生成代理类Java里有两种实现机制JDK动态代理要求目标类必须实现接口。它基于接口生成代理类通过InvocationHandler统一处理所有方法调用public class LogProxyHandler implements InvocationHandler { private final Object target; public LogProxyHandler(Object target) { this.target target; } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println(调用方法: method.getName()); long start System.currentTimeMillis(); Object result method.invoke(target, args); long cost System.currentTimeMillis() - start; System.out.println(方法执行耗时: cost ms); return result; } }CGLIB代理不要求实现接口通过生成目标类的子类来实现代理。Spring里如果你的bean没有实现任何接口默认就会走CGLIB。Spring AOP、Transactional的底层原理都是动态代理。理解了动态代理你才能真正理解为什么Spring的Transactional有时会失效——比如同类内部方法自调用的时候this.method()走的是真实对象而不是代理对象事务注解就形同虚设了。这个点面试经常问可以说是一个隐藏的加分项你不仅知道动态代理是什么还知道它导致的实际问题。4. 策略模式与模板方法摆脱if-else的两把板斧以及升级玩法策略模式是行为型模式里最实用的几个之一解决的核心问题是“同一种行为有多种算法/实现运行时动态选择”。支付场景是经典中的经典微信、支付宝、银联流程都是支付但验签、调接口、处理回调各不相同。4.1 传统策略模式接口 实现 上下文三个角色策略接口定义行为规范。具体策略类每个策略一个类。上下文/调用方持有策略引用并调用。public interface PayStrategy { void pay(BigDecimal amount); } public class WeChatPayStrategy implements PayStrategy { Override public void pay(BigDecimal amount) { System.out.println(调用微信支付接口金额: amount); } } public class AliPayStrategy implements PayStrategy { Override public void pay(BigDecimal amount) { System.out.println(调用支付宝接口金额: amount); } }配合一个上下文类动态传入策略public class PayContext { private final PayStrategy strategy; public PayContext(PayStrategy strategy) { this.strategy strategy; } public void executePay(BigDecimal amount) { strategy.pay(amount); } }这样做的效果是业务代码不再写if-else分支来判断渠道将来新增一个渠道只需要新增一个策略实现类。4.2 生产环境更实用的写法Map Spring注入传统的策略模式有个缺点是调用方要自己创建策略对象这在Spring项目里并不自然。我在实际项目里更推荐的做法是把策略实现类交给Spring管理然后用一个Map把渠道标识和策略Bean关联起来。Component public class PayStrategyFactory { private final MapString, PayStrategy strategyMap; public PayStrategyFactory(ListPayStrategy strategies) { this.strategyMap strategies.stream() .collect(Collectors.toMap( s - s.getChannel().getCode(), Function.identity() )); } public PayStrategy getStrategy(String channelCode) { PayStrategy strategy strategyMap.get(channelCode); if (strategy null) { throw new IllegalArgumentException(不支持的支付渠道: channelCode); } return strategy; } }所有实现了PayStrategy接口的Bean会被Spring自动收集进List然后转成Map。新增一个渠道就是新增一个加了Component注解的策略类连Factory都不用改真正达到了开闭原则。这种方式代码量不大但非常实用很多业务系统里就是这么干的。4.3 再叠加模板方法把策略里的公共流程抽出来策略模式有个常见痛点几个策略虽然实现不同但流程骨架是一样的。比如支付都是“参数校验 → 签名 → 调用接口 → 处理结果”只有中间某几步不同。这时候策略模式只解决了选择问题没有解决复用问题。我的建议是策略模式和模板方法模式结合使用。把流程骨架写在一个抽象父类里把变化的步骤声明为抽象方法让子类实现public abstract class AbstractPayStrategy implements PayStrategy { Override public void pay(BigDecimal amount) { validateParams(amount); String sign sign(buildRequestParams(amount)); String response callGateway(sign); handleResult(response); } protected abstract String sign(MapString, String params); protected abstract String callGateway(String sign); protected abstract void handleResult(String response); }这样一来公共逻辑只写一遍变化的步骤由各个子类实现。面试里聊到这个组合用法通常会让面试官眼前一亮——大多数人只停留在知道模式定义你能讲出模式之间的组合用法说明你真的实操过。4.4 函数式接口让策略模式的实现成本降到最低从Java 8开始很多传统策略模式可以用Lambda表达式替代。如果一个策略只有一个方法那它本质上就是一个函数。拿支付场景举例如果策略只是行为不同不需要持有状态可以直接用Map存函数MapString, ConsumerBigDecimal payActions Map.of( wechat, amount - System.out.println(微信支付: amount), alipay, amount - System.out.println(支付宝支付: amount) );当策略逻辑足够简单时用函数式写法比定义一堆策略接口和实现类更轻量。模式是活的这些对于面试聊“策略模式多种组合”的搜索意图来说就非常对口了。5. 观察者模式从手写事件回调到Spring的EventListener观察者模式解决的核心问题是“对象之间一对多的依赖关系——当一个对象状态变化时所有依赖它的对象都会收到通知”。典型业务场景下单成功后要触发发短信、发邮件、更新库存、赠送积分。如果这些都写在下单主流程里每加一个动作就要改核心代码甚至可能因为附加操作失败导致主流程失败。5.1 手写观察者模式理解发布订阅的思想本质Java原生的观察者模式有Observable和Observer但那已经是老古董实际生产一般不直接用。更常见的写法是自己定义事件和监听器// 1. 定义事件对象 public class OrderEvent { private final Long orderId; private final BigDecimal amount; public OrderEvent(Long orderId, BigDecimal amount) { this.orderId orderId; this.amount amount; } // getter省略 } // 2. 定义事件发布器 public class OrderEventPublisher { private final ListOrderListener listeners new CopyOnWriteArrayList(); public void register(OrderListener listener) { listeners.add(listener); } public void publishOrderEvent(OrderEvent event) { for (OrderListener listener : listeners) { listener.onOrderCreated(event); } } }核心思想是发布者持有监听器的集合事件发生时遍历通知。从设计模式的角度看观察者模式把“发生什么事”和“要做什么反应”解耦开了。5.2 在Spring里用EventListener更优雅如果你在Spring项目里其实Spring已经把观察者模式的框架搭好了。你用ApplicationEventPublisher发布事件用EventListener注解监听事件不需要自己维护监听器列表// 1. 发布事件 Service public class OrderService { private final ApplicationEventPublisher eventPublisher; public OrderService(ApplicationEventPublisher eventPublisher) { this.eventPublisher eventPublisher; } public void createOrder(Order order) { // 保存订单的业务逻辑 eventPublisher.publishEvent(new OrderCreatedEvent(order.getId(), order.getAmount())); } } // 2. 监听并按需处理 Component public class SmsListener { EventListener public void onOrderCreated(OrderCreatedEvent event) { // 发送短信 } } Component public class StockListener { EventListener public void onOrderCreated(OrderCreatedEvent event) { // 扣减库存 } }新增一个“下单后赠送优惠券”的动作只需要写一个新的监听器类完全不需要改OrderService。这就是观察者模式在真实框架里的实际落地。还要注意一点Spring事件默认是同步执行的。如果你希望短信发送等操作异步执行需要在监听器方法上加Async注解并且要搞清楚配置异步执行器的正确方式否则可能出现线程池耗尽或者注解不生效的问题。这块是实际开发中容易踩坑的一个点我把核心要点列一下启动类或配置类需要开启EnableAsync。异步监听方法所在类要能被Spring代理不能同类自调用。生产环境建议定义一个独立的业务线程池不要让所有异步任务共用一个池子。观察者模式的价值在于它让系统的“核心流程”和“周边动作”彻底分离。但也不要滥用如果只有两三个监听器而且事件从发布到处理都非常快那用不用观察者模式其实差别不大多一层事件机制反而增加排查难度。6. 什么时候不该用设计模式过度设计症状与识别指南聊完常用模式最后我想多说一句大实话——设计模式听得越多、学得越多越容易得“手里拿着锤子看什么都像钉子”的病。我见过太多代码本来一个if-else能讲清楚逻辑非要硬造一套工厂策略观察者的组合拳最后改一个bug要翻五个类。6.1 典型的滥用场景自查给你一张自查表照着看一下自己有没有这些毛病一个接口只有一个实现类却还是写了接口实现的组合。业务没有任何扩展迹象却提前预设了“以后可能”要支持多种情况。项目里没有Spring等框架支撑却自己手写一套工厂。一个类只有十几个方法却非要拆出一个抽象父类和三个子类。为了“统一”而“统一”把没有任何公共逻辑的类强行塞进同一个模板方法里。如果中了两条以上基本可以判断属于过度设计。6.2 什么情况下设计模式真的拯救了代码反过来如果你遇到下面这些特征说明上设计模式是对的已经有真实的第二个实现出现了原来的if-else开始越写越长。核心流程经常要加新步骤每次加都动主代码改一次错一次。对象创建逻辑复杂到调用方根本不需要知道细节。不同场景下同一行为的表现差异已经需要用配置文件或者参数来切换。设计模式是在需求演进过程中被“逼”出来的更优解而不是在项目启动时“设计”出来的装饰品。好的设计从来都是改出来的不是一次到位的。很多你看起来精妙绝伦的架构回看第一版往往是质朴到近乎简陋的。6.3 我的建议用“常用模式优先组合模式谨慎”的策略对大多数业务系统来说简单工厂、策略模式、模板方法、观察者、代理这五个模式基本覆盖了80%的日常需求。其他模式比如桥接、组合、享元、备忘录等更多是特定场景下的工具。我的学习路径建议是先精读Spring源码中BeanFactory、AOP的动态代理、事件机制、事务管理的实现从框架源码里看清这些模式是怎么被用“活”的再回头理解定义就变得很轻松。框架里的设计模式不是为了用而用它解决的是扩展性和解耦性的真实问题。我个人在实际项目里逐渐养成的一个习惯是写代码前先写注释注释里先讲清楚“这里未来可能有什么变化”再根据变化选模式。如果注释里找不出一个变化点那就说明这里不该用设计模式。这个习惯帮我挡掉了大量无效抽象也让我用设计模式的时候更有底气。拿不准的方案不妨先写最直白的版本等第二个真实需求出现再重构这比一开始就猜一个复杂的结构稳妥得多。真正的功力不在于记住多少种模式的名字而在于面对一段代码时能否看清楚它当前的位置、未来的变化方向然后只加恰到好处的那一层抽象。这与背了多少条框架命令无关却与能不能把一个系统的长期维护成本降下来很有关系。