2026/10/7 22:17:01

设计模式实战指南:从背模式到用模式,Java实现与项目落地

设计模式实战指南:从背模式到用模式,Java实现与项目落地 设计模式这东西一到期末或者大作业节点就会被疯狂搜索但我发现大多数人是把它当成“背诵清单”来学的。23个模式挨个背定义、背UML图、背代码模板背完就忘问起来每个都知道一点真放到项目里一个也不敢用、也不知道在哪用。我自己带过团队、也面试过不少人能明显感觉到“背过模式”和“会用模式”完全是两回事。这篇就把我带新人时反复讲的东西整理出来结合Java实现把模式分类、核心思想、大作业/真实项目的落地选型、还有那些常踩的坑一次说清楚。适合正在学设计模式应付期末或者大作业的同学也适合工作几年但感觉代码一直绕不开if-else泥潭、想系统性提升一下的开发者。1. 为什么要学设计模式它解决的是你代码的“变”与“不变”先说个实际场景。你接了一个需求系统里有两种支付方式支付宝和微信当前是if-else二选一。过了一个月产品说要加银联又过一个月要加花呗再往后可能还要加各种各样的分期渠道。如果你每次都往原来的代码里塞一个else分支你的支付类会越来越长改一次就得把整个接口的测试全部回归一遍这就是典型的“面向修改”编程——每次需求变更都在动旧代码风险极高。设计模式的价值不在于它给了你23个花哨的类名而在于它逼你想清楚一个问题哪些东西是稳定的哪些东西是会变的怎么把会变的部分隔离出去。你仔细观察那23个模式就会发现不管哪个分类它们本质上都在做同一件事——用“组合”和“封装”来换取“可扩展”和“可维护”。支付方式会变那定义一个支付接口、把具体支付方式做成实现类这就是策略模式的雏形创建具体支付对象的逻辑会变那把它交给一个工厂这就是工厂模式支付完成后的状态流转会变那把它抽成状态机这就是状态模式。所以学设计模式的第一课不是背类名和UML图而是建立这种变与不变的直觉。拿到一个需求先别急着写代码先问自己这个模块未来可能以什么方式变化把这些变化点找出来再去匹配看哪个模式能帮你隔离它。很多人还会问现在Spring这些框架里到处都是设计模式我不学照样能开发这话对也不对。你用Spring的时候依赖注入是工厂模式的思想AOP是代理模式的思想模板方法在Spring的JdbcTemplate里体现得淋漓尽致——你确实在用它们但你是“无意识地用”。一旦出现问题比如事务失效、代理不生效、Bean的加载顺序不对如果你完全没有模式层面的认知排查起来就像在没有地图的城市里找一条巷子能靠的只有瞎试。反过来当你理解了动态代理和JDK代理、CGLIB代理的区别Spring很多报错你一眼就能看出问题在哪。适配人群我按自己带新人的经验说纯小白想直接入门建议先别死磕全部23个把重点模式吃透再去覆盖其余的处于期末/大作业阶段的同学重点放在怎么用“模式的思想”梳理你的代码结构已经有工作经验的重点放在“重构时识别坏味道并引入模式”上。你的阶段不同学法和侧重点完全不同别一刀切。2. 23种设计模式到底怎么分类和理解2.1 三大分类的真正意义名字可以直接告诉你“意图”设计模式的经典分类是创建型5种、结构型7种、行为型11种。很多人背这个分类只为了应付名词解释但实际上这个分类本身就是一个检索目录看到“创建”就知道它管的是对象怎么来的看到“结构”就知道它管的是类和对象怎么组合成更大的结构看到“行为”就知道它管的是对象之间的职责怎么分配和通信。创建型5种单例、工厂方法、抽象工厂、建造者、原型。往细了说单例是“全局只许有一个”工厂是“创建逻辑不暴露给调用方”抽象工厂是“一系列相关对象的整体生产”建造者是“对象的组装过程和表示分离”原型是“通过复制现有对象来创建新对象”。结构型7种适配器、桥接、装饰器、组合、外观、享元、代理。适配器是“让接口不匹配的类能一起工作”桥接是“抽象部分和实现部分分离”装饰器是“动态地给对象添加职责”组合是“把部分-整体关系树状处理”外观是“为复杂子系统提供统一入口”享元是“共享细粒度对象减少内存”代理是“为真实对象提供一个替身来控制访问”。行为型11种模板方法、策略、观察者、迭代器、状态、命令、责任链、解释器、中介者、备忘录、访问者。它们管的是“算法流程的骨架”模板方法、“算法的选择”策略、“对象状态变化的通知”观察者、“状态驱动的行为切换”状态、“请求的发送和接收解耦”命令等。我建议你把这个分类表当成索引图记而不是纯背。先在脑中建好这个图再往每一个结点里填“它解决什么问题、核心类有哪几个、在哪里见过它”这样比死记硬背牢固得多。2.2 哪些模式最值得优先吃透我从来不让新人一上来就背全部23个那既不现实也没意义。按实际出现频率我列一个优先级一等必须吃透面试和项目都高频单例、工厂方法、策略、观察者、装饰器、适配器、模板方法、代理。二等很常用建议掌握抽象工厂、建造者、状态、责任链、外观、组合、迭代器。三等用了场景就很好但平时不易遇到桥接、享元、命令、中介者、备忘录、访问者、解释器、原型。这个优先级不是我拍脑袋排的它背后对应的是Java开发里对“接口隔离”“全局控制”“行为扩展”“算法复用”的需求最旺盛所以这些模式出镜率最高。像解释器和访问者日常业务开发里大概率一年用不到一次面试简单了解即可时间应该花在刀刃上。2.3 用一张表快速建立“模式→场景”的映射模式核心解决典型场景单例全局唯一实例配置类加载、连接池、日志器工厂方法创建和使用解耦不同支付方式、不同消息渠道的创建抽象工厂一系列相关产品不同UI主题生成对应系列控件建造者组装复杂对象创建复杂配置对象、构建HTTP请求体原型复制已有对象场景中创建大量类似对象、深拷贝/浅拷贝适配器接口兼容老接口适配新调用方、第三方SDK封装装饰器动态增强能力Java IO流的层层包装组合部分与整体文件目录树、菜单树、组织架构树外观统一门面一个Service封装多个底层Manager代理控制访问AOP切面、延迟加载、远程代理模板方法算法骨架复用流程固定但步骤实现不同的业务策略算法可替换优惠计算、校验规则、渠道发送观察者状态变化通知订单状态变更后触发多步后续操作状态行为随状态切换订单状态机、审批流程、游戏角色状态责任链请求逐级处理多级审批、日志级别过滤、网关过滤器链这里先建立一个整体地图接下来的部分我会挑重点的几个拆开讲清楚再把它们串到大作业和实际项目里去。3. 核心模式逐个拆解原理、场景、Java实现3.1 单例模式不是“只有一个对象”那么简单单例模式是最容易被人低估的模式。“全局只有一份”很多人理解为“为了节省内存”其实对于JVM里对象开销的情况来说很多时候真正的原因是需要共享状态或者保证对资源访问的一致性。比如一个全局配置类多个线程往里读同一个数据源如果不统一成单例各读各的实例配置变更时数据就乱了。Java里实现单例有几种常见写法踩坑最多的是“懒汉式”的并发问题。最安全也最省事的写法是枚举单例它在序列化和反射攻击上天然安全是我个人最推荐的方式public enum ConfigManager { INSTANCE; private String configValue; public void setConfigValue(String value) { this.configValue value; } public String getConfigValue() { return configValue; } }但很多面试官和作业考核偏好“双重检查锁”你需要知道它为什么加volatile。先看代码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有两个作用一是禁止指令重排序因为new ConfigManager()不是原子操作它包含分配内存、初始化对象、把引用指向内存三个步骤CPU和编译器可能重排导致另一个线程拿到一个“未完成初始化”的对象二是保证可见性让其他线程能第一时间看到instance已经被赋值了。很多人面试栽在这里就因为没有理解volatile不是“锦上添花”而是这个写法成立的前提。3.2 工厂模式把“new”收拢到一处新手最容易犯的坏习惯是全代码散落着一堆new Xxx()。这本身不是错误但一旦产品说“我们要把原来的A替换成B”你就得把所有new A()的地方找出来改。工厂模式的核心诉求就是让创建对象这件事收敛到一个类里调用方只认接口不关心背后具体是谁。一个最朴素的简单工厂写法public interface Payment { void pay(BigDecimal amount); } public class AliPay implements Payment { Override public void pay(BigDecimal amount) { System.out.println(支付宝支付 amount); } } public class WeChatPay implements Payment { Override public void pay(BigDecimal amount) { System.out.println(微信支付 amount); } } public class PaymentFactory { public static Payment create(String channel) { if (ali.equals(channel)) { return new AliPay(); } else if (wechat.equals(channel)) { return new WeChatPay(); } throw new IllegalArgumentException(不支持的支付渠道); } }调用方变成了Payment payment PaymentFactory.create(channel); payment.pay(amount);这样还不够因为每个新支付渠道上线时还要改工厂类的if-else。更进阶的做法是用一个注册表来管理实现类通过反射或者Spring的依赖注入自动注册。比如用一个Map存渠道名和ClassComponent public class PaymentFactory { private final MapString, Payment map new HashMap(); Autowired public PaymentFactory(ListPayment payments) { for (Payment p : payments) { map.put(p.channelName(), p); } } public Payment get(String channel) { return map.get(channel); } }这时候再来新支付方式只要写一个新的实现类标注渠道名工厂一行都不用改开闭原则才真正落地。大作业里如果你用Spring强烈建议用第二种方式。3.3 策略模式消灭“if-else雨”的利器你代码里最常见的坏味道是什么我猜是二选一或多选一。比如根据订单类型计算价格根据用户等级算折扣根据文件类型走不同的解析逻辑。每多一种新情况就多加一个else几轮迭代之后方法变得又臭又长。策略模式解决的就是这个问题。它的结构不复杂一个策略接口一组策略实现类以及一个持有当前策略的上下文Context。Java里因为有了函数式接口策略模式甚至可以用lambda简化。比如促销折扣计算public interface DiscountStrategy { BigDecimal apply(BigDecimal price); } public class VipDiscount implements DiscountStrategy { Override public BigDecimal apply(BigDecimal price) { return price.multiply(new BigDecimal(0.8)); } } public class NormalDiscount implements DiscountStrategy { Override public BigDecimal apply(BigDecimal price) { return price; } } // 调用方 DiscountStrategy strategy new VipDiscount(); BigDecimal result strategy.apply(originalPrice);用策略模式替换if-else的关键点是策略的选择逻辑可以放到工厂或者一个Map里调用的地方只管拿到一个策略来执行这样新增一个策略类型时业务调用方的方法完全不用动。有一个很重要的经验当你的策略数量超过8个甚至更多时又会出现“策略类的爆炸”——每个类特别薄但类数量很多。这时候可以结合查表法把策略定义成枚举内的一个字段或者用函数式接口直接存储lambda减少类数量。3.4 观察者模式让“联动逻辑”不再硬编码最常见的观察者场景就是事件发布。比如订单支付成功后要同时做发短信、记日志、更新库存、推送客服消息。如果你在支付成功的方法里一个个去调用这些服务每个新的联动需求都要进支付方法里加代码这就是高耦合。观察者模式的思路是支付成功只是一个“事件”具体谁关心这个事件、收到事件后做什么通知者根本不关心。通知者只需要维护一个监听器列表事件发生时遍历通知即可。Java里最简单的实现可以这样public interface OrderEventListener { void onOrderPaid(Long orderId); } public class OrderService { private final ListOrderEventListener listeners new ArrayList(); public void registerListener(OrderEventListener listener) { listeners.add(listener); } public void payOrder(Long orderId) { // 核心支付逻辑 System.out.println(订单 orderId 支付成功); // 通知所有监听者 for (OrderEventListener listener : listeners) { listener.onOrderPaid(orderId); } } }Spring环境下更简单直接发一个ApplicationEvent。// 事件类 public class OrderPaidEvent extends ApplicationEvent { private Long orderId; // 构造器略 } // 订阅方 Component public class SmsListener { EventListener public void onOrderPaid(OrderPaidEvent event) { // 发短信 } }这个模式在你写大作业时很实用比如一个简单的博客系统用户发布文章后要“更新用户积分”“给关注者生成通知”“刷新首页缓存”用事件发布一次搞定后续加新功能不用再动发布文章的Service方法。3.5 装饰器模式Java IO的“套娃”之谜你是不是经常看到Java的IO代码长这样BufferedReader reader new BufferedReader(new InputStreamReader(new FileInputStream(a.txt)));很多人第一次看到这段代码会疑惑怎么一层套一层其实这就是装饰器模式的经典例子。FileInputStream是数据源InputStreamReader给它增加了字节转字符的能力BufferedReader又给它加了缓冲能力。每一层都在不改变原对象的情况下给对象加新能力。装饰器和继承的区别在于继承是编译期决定的扩展是写死的装饰是运行期动态组合的你可以任意搭配。你要带缓冲的字符文件流就按上面的顺序套你要带缓冲的字节流就是BufferedInputStream(new FileInputStream(...))完全由你自由组合。自己写一个装饰器记住四点装饰器和被装饰者实现同一个接口或者继承同一个抽象类。装饰器内部持有被装饰者对象。装饰器在调用被装饰者方法的前后插入新逻辑。装饰器不会改变对象的类型。比如给一个已有的支付接口加“打点日志”能力public class LoggedPayment implements Payment { private final Payment delegate; public LoggedPayment(Payment delegate) { this.delegate delegate; } Override public void pay(BigDecimal amount) { System.out.println(开始支付金额 amount); delegate.pay(amount); System.out.println(支付完成); } }这就是AOP“切面思想”的雏形理解了装饰器再去看Spring AOP里那套前置通知、后置通知就很好懂了。3.6 模板方法模式把流程骨架钉死把可变化部分留子类如果你经常处理流程类的业务——比如一次数据迁移要经过“读取数据→校验数据→转换数据→写入数据”四个步骤里面每个步骤都可能因为来源不同而变化但整体流程固定不动模板方法模式就是为这类场景准备的。public abstract class DataMigrator { // 模板方法流程骨架固定 public final void migrate() { ListString rawData readData(); ListString validData validate(rawData); ListString convertedData convert(validData); writeData(convertedData); } protected abstract ListString readData(); protected abstract ListString validate(ListString rawData); protected abstract ListString convert(ListString validData); protected abstract void writeData(ListString data); }子类只需要实现各自的读写和转换细节流程不允许重写所以模板方法用final修饰。这个模式在重构“多个业务方法长得像但中间某几步不同”的代码时极其好用它几乎没有学习成本却能让重复代码大幅下降。4. 如何把设计模式用到大作业和真实项目中4.1 “设计模式大作业”到底要写什么才能拿高分每年都有同学来问设计模式大作业到底做一个什么题目才好。我给的建议是不要做一个“为了模式而模式”的玩具系统而要做一个业务上天然就有变化点的系统。比如点餐系统、订单系统、博客系统、支付模块、审批流系统——这些里面天然存在“多支付方式”“多订单状态”“多折扣规则”你不需要刻意编造业务模式的用武之地自己就冒出来了。拿一个咖啡馆点餐系统举例它的模式落点是这样的入口处选择饮品用简单工厂/工厂方法统一创建不同饮品对象。加料加奶泡、加双份浓缩、加焦糖用装饰器模式逐层包装出一个定制饮品。不同时段折扣不同会员价、工作日下午茶价、普通价用策略模式。用户下单后系统需要完成“打印小票、通知后厨、更新库存、推送短信”用观察者模式。整个订单状态待支付→制作中→已完成→已取消用状态模式。如果有多个优惠同时满足条件需要按优先级逐条尝试用责任链模式。这种大作业的答辩是很好过的因为你每个模式都不是硬塞的而是“这个地方真的存在变化点所以我用该模式把变化隔离了”。答辩的时候你就用这句话解释评委挑不出毛病。4.2 选择模式的实操判断流程很多人的困惑是我看完了23个模式遇到一个具体问题还是不知道选哪个。我教你一个我自己用的三步判断法第一步先看“需求变化点”是什么。如果变化点集中在“创建方式”比如新增一种支付方式走创建型如果集中在“对象间的协作方式”比如订单状态一变化就要做一堆事走行为型如果集中在“对象如何组装成更大的结构”比如一个复杂的视图组合树走结构型。第二步再问“变化点怎么变”。如果是“一个接口多个实现运行时替换”——策略如果是“一个核心功能前后追加额外动作”——装饰器或模板方法如果是“多个对象按顺序处理一个请求”——责任链如果是“一个对象状态变化时想通知别的人”——观察者如果是“一个对象的内部状态决定行为”——状态。第三步动手画一个最简单的草图有几个类谁持有什么谁调谁。画完之后你基本能确认该不该用这个模式。如果你画出来之后发现要引入七八个类才解决一个原本两行的逻辑那先停下来——也许这里只需要写一个简单的if。设计模式不是越多越好它是用来简化维护的不是用来炫技的。4.3 实战代码一个订单状态机的状态模式实现状态模式是很多人觉得抽象的一个模式实际写一个订单状态机就很容易理解了。一个订单有空状态待支付、已支付、配送中、已完成、已取消。每个状态下能做的操作不一样。比如“待支付”状态可以“支付”和“取消”但不可以“发货”“已支付”状态可以“发货”但不能再“支付”。如果不用状态模式你会写一个巨大的状态枚举加一坨switch-case。用了状态模式每个状态就是一个类public interface OrderState { void pay(OrderContext context); void ship(OrderContext context); void complete(OrderContext context); void cancel(OrderContext context); } public class UnpaidState implements OrderState { Override public void pay(OrderContext context) { System.out.println(订单支付成功); context.setState(new PaidState()); } Override public void ship(OrderContext context) { throw new IllegalStateException(未支付不能发货); } Override public void complete(OrderContext context) { throw new IllegalStateException(未支付不能完成); } Override public void cancel(OrderContext context) { System.out.println(订单已取消); context.setState(new CancelledState()); } }OrderContext持有当前状态对外暴露pay、ship等方法内部把调用转给当前状态对象public class OrderContext { private OrderState state new UnpaidState(); public void setState(OrderState state) { this.state state; } public void pay() { state.pay(this); } public void ship() { state.ship(this); } }以后新增状态“退款中”只需要新增一个类定义好它各个操作的走向原有代码保持不动。这就是状态模式的价值。4.4 大作业答辩时容易被追问的“坑”答辩时最常见的一个问题是“你这个模式可以换成另一个模式吗比如策略换成状态行不行”你需要心里有数策略模式强调的是“算法自由切换”切换的动作是由外部发起的状态模式强调的是“状态内部自行决定下一个状态”状态之间的流转是状态对象自己主导的。订单状态选择用状态模式而不是策略模式是因为支付成功后状态自动变为已支付这个流转逻辑放在状态类内部更内聚而折扣计算用策略是因为选择哪种折扣由外部条件决定策略本身不负责切换自己。另一个坑点是“单例模式会不会造成性能问题”。如果你用了锁高并发下getInstance会有竞争但这个性能问题在对象初始化完毕之后就不存在了。如果担心初始化耗时可以用枚举或者静态内部类的方式都会更优雅public class LazySingleton { private LazySingleton() {} private static class Holder { private static final LazySingleton INSTANCE new LazySingleton(); } public static LazySingleton getInstance() { return Holder.INSTANCE; } }静态内部类的实现既保证了懒加载又避免了同步开销是我自己平时最常用的一种单例写法。5. 常见问题排查与学习避坑实录5.1 学习阶段最容易遇到的几个问题问题一模式看完就忘合上书脑子里一片空白。这太正常了。我建议每学完一个模式就立刻做三件事用一个生活中的例子描述它比如适配器就是“苹果手机的Type-C转接头”写一个最小可运行Java示例在你的大作业代码里找出一个它可能应用的场景。三件事都做完这个模式才算进了你的长期记忆。问题二感觉很多模式长得非常像分不清。最高频混淆是三组工厂方法 vs 抽象工厂策略 vs 状态装饰器 vs 代理。我自己的区分口诀是工厂方法解决“一个产品”的衍生抽象工厂解决“一套产品”的配套策略是“外部让你换算法”状态是“内部自己换状态”装饰器是“增强功能”代理是“控制访问”。记住这几点考前基本不会混乱。问题三UML图不会画。考试和大作业都要求画类图。你不需要记所有符号细节核心抓住几种关系实线空心三角是继承方向虚线箭头是依赖实线箭头是关联。一个类图能表达出“一个接口、几个实现类、一个类持有接口”就已经涵盖了大部分模式的本质。问题四用Java函数式接口lambda写策略时接口只有一个方法还能叫策略模式吗计算器、校验器、折扣器这类本来就只有一个方法lambda简化之后依然成立。千万不要觉得“必须每个实现类都写一个大Class文件”能用lambda的用lambda代码更清爽。5.2 项目里套用模式失败的常见原因模式失败最常见的不是用错模式而是模式套的时机太早。项目需求还没成型就把观察者、代理、命令这些全部安排上最后发现多了一堆空转的抽象类任何一次改动要牵动七八个文件。我个人的经验是先在代码里写直观的版本当发现某个地方因为重复或者因为新增需求不断修改出问题的时候再重构引入模式。这叫“从坏味道倒推出模式”而不是“每个需求都硬套模式”。第二个常见失败原因是模式之间的过度嵌套。比如为了处理订单先套了个工厂来创建策略策略里又通过模板方法定义流程模板方法里又调用了观察者事件。听上去很“架构”实际上排错时你会痛不欲生。简单直接的代码永远优先于“优雅”的抽象。写完一段代码你多问自己一句“这个抽象有没有让我的改动成本真的下降”如果答案是否定的就拆掉一半。第三个失败原因是忽视了模式会对测试造成的影响。单例模式会让单元测试之间共享状态容易产生测试间相互影响观察者模式如果不注意监听器的注册和反注册会产生内存泄漏或者重复通知。所以做设计的时候就要想好测试策略比如单例类要提供重置状态的缝隙观察者要提供取消注册的方法。5.3 复习冲刺期末必考的高频点清单如果明天就要考试我给你圈几个高频考点省得再去全网翻UML类图基础依赖、关联、聚合、组合的区别实现和继承的画法。这道题基本是设计模式考试的首道大题。单例模式的几种写法和并发安全性饿汉、懒汉、双重检查锁、静态内部类、枚举各自的优缺点必须说得出来。工厂模式三种形态的概念区分简单工厂不是GoF的23种之一但考试很爱考工厂方法和抽象工厂的语义边界要清楚。装饰器与适配器的区别装饰器是不改变接口、增强功能适配器是改变接口以匹配新调用方。策略与状态、策略与模板方法的区别策略是选择模板方法是固定流程这两者一个靠“替换”一个靠“继承”容易混。观察者模式的JDK实现Java的Observer/Observable已经成了废弃API但它曾经是经典考题还是要了解。更该掌握的是Spring的EventListener用法。代理模式与Spring AOP的关系JDK动态代理只能代理接口CGLIB可以代理类这是Spring面试常问的基础。6. 写在最后我踩过几次坑之后的一些结论设计模式学了不会立刻让你写出惊为天人的代码但对代码的“未来感觉”会有明显变化。我感触最深的是设计模式不是一个“使用指南”它是一个“重构指南”——大多数优秀的设计不是一上来就规划出来的而是在不断演进中逐步形成的。你代码里出现了长长的if-else你把它抽成策略你发现多处重复的流程骨架你把它抽成模板方法你发现一个类做着好几件不相关的事你把它按职责拆开再通过观察者或事件去协作。这个过程你已经是在运用设计模式的思想了。当年带我入行的前辈说了一句我现在依然常拿来说的话设计模式最好的使用时机是你觉得“这段代码被需求牵着走、改得越来越痛苦”的时候。它像一个提前准备好的工具箱关键不是在墙上挂满工具而是当你真的拧不下一颗螺丝时能想起来这个工具的存在。如果你正在写大作业我的最后一个建议是不要追求“把所有23个模式都用进去”这种极端。模式是一个工具作业最后呈现出来的代码应该是“别人读完感觉像一篇有条理的文章”而不是“看完了只觉得五花八门但抓不住主线”。挑四到六个和你业务真正契合的模式把它们用得干净彻底远比堆十来个模式更能说明你理解了它。把这个理念带到项目里你以后写出的代码会在每一次需求变更时让你少掉一把头发。