设计模式不虚:23种模式实战、面试速记与重构经验 我以前一直觉得设计模式这东西很虚直到有一次在代码评审会上被同事一句话问住“你觉得这段300行的if-else下个月新需求来了还能不能改”当时我写的是一个业务系统的计费逻辑自认为逻辑清晰、注释到位没想到一碰需求变更就崩。从那时候起我开始系统性地啃23种设计模式啃完之后回头看那段代码发现很多问题其实根本不需要靠“头铁”硬扛而是有成熟套路可循的。这篇东西我就把这几年实际用设计模式的经验、踩过的坑、面试速记的技巧、还有期末和软考复习的思路都整理出来给还在背定义、背UML图的你一条更接地气的路。1. 设计模式到底是什么从一句“看不懂的代码”说起1.1 设计模式不是算法是“沟通语言”很多人第一次接触设计模式会下意识把它和数据结构、算法混在一起觉得“这又是要背的一堆东西”。其实完全不是一回事。算法解决的是“这一步怎么算”设计模式解决的是“这一坨代码怎么摆”。我的理解是设计模式本质上是一套约定俗成的命名体系和结构模板。什么意思呢我举个生活例子。你去一家餐厅后厨看到厨师看一眼单据就知道先备哪个菜、哪个灶台出哪个品不是因为厨师记忆力有多好而是后厨有一套固定的分工和流程。软件也一样当所有人都知道“看到XxxFactory就知道这段代码负责创建对象”、“看到XxxObserver就知道这段代码在监听某个事件”代码就不需要逐行解释了。这也是为什么设计模式会被叫做“沟通语言”。它降低的不是运行时的开销而是人与人之间的理解成本。你写代码可能只有一个人但读代码的可能是三个月后的你自己也可能是接手你项目的同事。让代码表达出“意图”比任何注释都管用。1.2 设计模式背后的五大原则你要是直接背23个模式会发现背完就忘了因为模式太多、变体更多。我的建议是先搞懂五条原则模式只是这些原则的具体落地。这五条原则分别是单一职责原则一个类只干一件事。好比一个厨师只管炒菜洗碗是另一个人的事。开闭原则对扩展开放对修改关闭。加新功能尽量不碰旧代码像手机壳一样换个壳子不用拆主板。里氏替换原则子类应该能无缝替换父类。你把接口的实现类换掉调用方不该感知到任何异常。接口隔离原则接口别贪多别把不相关的方法塞在一起否则实现类得被迫实现一堆用不到的方法。依赖倒置原则高层模块不依赖底层细节大家都依赖抽象。你看插座电器不直接接电线而是通过插头接口对接。我当时学到这里想通了一件事设计模式不是“规矩”而是这些原则在特定场景下的标准答案。比如策略模式就是为了开闭原则服务的你加一种新策略就多一个类不用去改动原来的业务类。所以如果你面试被问到“为什么用策略模式”别只背定义把开闭原则带出来面试官立刻会觉得你是真懂。1.3 三种分类不是理论是三种看问题的角度GoF把23种模式分成三类这个分类不是为了考试而是在回答软件设计中最基本的三个问题分类解决的核心问题生活类比创建型对象怎么被创建你开餐厅食材从哪个供应商进结构型类和对象怎么组合你搭积木积木块怎么拼成城堡行为型对象之间怎么协作餐厅后厨传菜员、厨师、前台怎么配合我当时用这个角度去理解一下就通了。创建型管的是“new”这个动作为什么连new都要管因为有的对象创建成本高比如数据库连接有的对象创建条件复杂比如根据配置创建不同对象有的对象只想被创建一次。结构型管的是“类和类之间怎么组成更大的结构”是继承还是组合是包一层还是换一个接口。行为型管的是“多个对象之间怎么传消息、怎么分配责任”这是最抽象的一类也是实际工作中用得最多的一类。分类这块不需要死记你只需要记住一个软件系统的设计过程本质上就是不断回答“对象怎么来、怎么放、怎么协作”这三个问题的过程。2. 高频模式逐个拆解这些模式真的天天用2.1 创建型单例、工厂、建造者先说单例模式这可能是面试出现频率最高的一个模式。单例解决的是“全局只需要一个实例”的问题典型的场景包括配置管理器、连接池、线程池。Spring容器里默认的Bean就是单例所以你在业务开发里基本天天都在用。单例的写法有很多种我推荐最稳妥的**双重检查锁DCL**写法public class ConfigManager { private static volatile ConfigManager instance; private ConfigManager() { // 私有构造防止外部new } public static ConfigManager getInstance() { if (instance null) { // 第一次检查避免无谓加锁 synchronized (ConfigManager.class) { if (instance null) { // 第二次检查防止并发重复创建 instance new ConfigManager(); } } } return instance; } }这里有个细节很多人忽略为什么要加volatile因为new ConfigManager()在底层不是一步完成的它分三步分配内存、初始化对象、把引用指向内存。JVM允许指令重排序如果不加volatile线程A可能先完成引用的赋值而对象还没初始化完成线程B此时拿到引用去用就会出问题。我当年就是没想明白这一点面试被问倒过一次后来看《Java并发编程的艺术》才彻底搞懂。再说工厂模式。工厂模式的价值一句话总结把new这件事从业务代码里剥出去。比如你做一个支付系统最开始只有支付宝支付直接new AlipayService()没问题。后来加了微信支付于是代码变成if (payType.equals(alipay)) { payService new AlipayService(); } else if (payType.equals(wechat)) { payService new WechatService(); }再加银联、花呗这个if-else会越来越长。这时候用简单工厂public class PayServiceFactory { public static PayService create(String payType) { if (payType.equals(alipay)) { return new AlipayService(); } else if (payType.equals(wechat)) { return new WechatService(); } throw new IllegalArgumentException(unsupported payType: payType); } }虽然简单工厂还是有if-else但已经把创建逻辑从业务类里隔离出来了。业务那边只需要一行PayService payService PayServiceFactory.create(payType);。如果分支继续膨胀可以进一步演进为工厂方法模式把创建动作下沉到子类。建造者模式我最早觉得它很鸡肋直到遇到一个参数爆炸的场景。假设你要创建一个查询条件对象有分页、排序、过滤、是否包含已删除等十几个属性用构造函数传参的话你写new SearchRequest(1, 20, createTime, true, false, null, ...)谁看谁懵。用建造者模式就清晰多了SearchRequest request SearchRequest.builder() .page(1) .pageSize(20) .orderBy(createTime) .includeDeleted(false) .build();每个参数都有名字读写都很直观。这个模式的核心是当对象的构造参数多到一定程度就别再硬塞构造函数了。2.2 结构型装饰器、代理、适配器结构型模式里我工作中用得最多的是装饰器模式。你可能没意识到Java IO的类设计就是装饰器模式最经典的例子new BufferedInputStream(new FileInputStream(test.txt))。为什么不用继承因为继承会让类爆炸。假设你有3种输入流、4种增强功能继承的话要组合出12个子类用装饰器的话4个装饰类就能随意组合。装饰器的关键点是装饰类和被装饰类实现同一个接口通过构造器传入被装饰对象然后在调用时增强逻辑。举个简单的例子public interface DataSource { void write(String data); } public class FileDataSource implements DataSource { // 具体写入文件逻辑 } public class EncryptionDecorator implements DataSource { private final DataSource wrapper; public EncryptionDecorator(DataSource wrapper) { this.wrapper wrapper; } Override public void write(String data) { String encrypted ENC( data ); // 增强逻辑 wrapper.write(encrypted); } }用的时候可以一层层套new EncryptionDecorator(new FileDataSource())既写了加密又写了文件互不干扰。这种“组合优于继承”的思路就是结构型模式的核心精神。代理模式和装饰器长得很像但意图完全不同。装饰器是给对象增加功能代理是控制访问。最典型的场景是Spring AOP你写一个Transactional注解Spring会帮你生成一个代理对象在方法前后自动开启和提交事务。Spring AOP默认用的是JDK动态代理基于接口实现。手写一个静态代理也很简单public interface UserService { void login(String username); } public class UserServiceImpl implements UserService { Override public void login(String username) { System.out.println(username 登录成功); } } public class UserServiceProxy implements UserService { private final UserService target; public UserServiceProxy(UserService target) { this.target target; } Override public void login(String username) { long start System.currentTimeMillis(); target.login(username); System.out.println(耗时 (System.currentTimeMillis() - start) ms); } }这样就能在不改原类的情况下加日志、加监控、加权限控制。适配器模式也很实用它的作用是“让不兼容的接口能够合作”。比如你接了一个第三方短信SDK它的方法名是sendSms()而你们项目统一的方法是sendMessage()你不想让业务代码直接依赖SDK就可以写一个适配器public class SmsAdapter implements MessageSender { private final ThirdPartySmsSender thirdPartySmsSender; public SmsAdapter(ThirdPartySmsSender thirdPartySmsSender) { this.thirdPartySmsSender thirdPartySmsSender; } Override public void sendMessage(String content, String mobile) { thirdPartySmsSender.sendSms(mobile, content); } }这个模式的意义在于保护业务层不被外部库绑架。以后要换短信供应商只需要再写一个适配器业务代码一行都不用动。2.3 行为型策略、观察者、模板方法策略模式是我个人认为性价比最高的一个模式。它解决的是“同一件事不同的做法”的问题。还是拿支付举例前面用工厂解决了“创建什么支付对象”策略模式则负责“怎么执行支付逻辑”。通常这两个模式会配合使用public interface PayStrategy { void pay(BigDecimal amount); } public class AlipayStrategy implements PayStrategy { Override public void pay(BigDecimal amount) { // 支付宝支付逻辑 } } public class WechatPayStrategy implements PayStrategy { Override public void pay(BigDecimal amount) { // 微信支付逻辑 } } // 使用方 public class OrderService { private final MapString, PayStrategy strategyMap; public OrderService(MapString, PayStrategy strategyMap) { this.strategyMap strategyMap; } public void checkout(String payType, BigDecimal amount) { PayStrategy strategy strategyMap.get(payType); if (strategy null) { throw new IllegalArgumentException(unsupported payType); } strategy.pay(amount); } }这样以后新增一个云闪付只需要写一个YunShanPayStrategy并注册到Map里OrderService的代码完全不用动。这就是典型的开闭原则落地。观察者模式解决的是“一件事发生了多处要响应”的问题。比如用户注册成功后要发欢迎短信、送优惠券、记录日志。如果在注册方法里把这三个操作全写一遍以后每加一个动作就得改一次注册方法早晚改出bug。观察者模式的做法是注册方法只负责发“事件”订阅者各自处理public class UserRegisteredEvent { private final Long userId; // getter / setter } public interface EventListener { void onEvent(UserRegisteredEvent event); } public class SmsListener implements EventListener { Override public void onEvent(UserRegisteredEvent event) { // 发送短信这里只关心自己的逻辑 } } public class CouponListener implements EventListener { Override public void onEvent(UserRegisteredEvent event) { // 发放优惠券 } }Spring里的事件机制就是这么设计的比如ApplicationEventPublisher发布事件EventListener监听事件。你只需要维护监听器的注册列表不用改发布方的代码。模板方法模式则是把“流程骨架”固定住把“可变步骤”留给子类。比如项目上线都有固定的流程备份代码、停服、发布、重启、验证。但不同项目验证的方式不一样有的看接口返回有的查数据库。这时候就可以定义一个模板public abstract class DeployTemplate { public final void deploy() { backup(); stopServer(); publish(); restart(); validate(); } protected abstract void validate(); // 可变步骤看子类自己的验证逻辑 private void backup() { /* 通用逻辑 */ } private void stopServer() { /* 通用逻辑 */ } private void publish() { /* 通用逻辑 */ } private void restart() { /* 通用逻辑 */ } }模板方法模式的好处是流程不容易被破坏子类只需要关心自己要变化的那一步。在很多框架源码里模板方法模式出现得极其频繁比如Spring的JdbcTemplate就是固定了“获取连接、执行SQL、关闭连接”的模板把“解析结果集”交给回调。3. 速记与面试怎么记、怎么答、怎么避坑3.1 23种设计模式记忆口诀很多读者是冲着“软考”和“期末”来的我懂你们最缺的是一套能快速回忆模式的记忆法。我整理了一个口诀配合表格用复习效果会好很多。口诀我拆成三组创建型5个单工建原抽单例、工厂方法、建造者、原型、抽象工厂结构型7个适桥组外代装享适配器、桥接、组合、外观、代理、装饰器、享元行为型11个责命状策模、观迭中解访备责任链、命令、状态、策略、模板方法、观察者、迭代器、中介者、解释器、访问者、备忘录注意简单工厂通常被归为“编程习惯”而不算GoF 23种模式之一上面口诀里我用的是“工厂方法”而不是简单工厂。考试时如果问23种设计模式一定要按GoF的分类来答。下面这张分类表我建议你考前反复看几遍把模式名称和“一句话意图”对应上就够了分类模式一句话意图创建型单例全局只有一个实例创建型工厂方法子类决定创建哪种对象创建型抽象工厂创建一族相关对象创建型建造者分步骤构造复杂对象创建型原型通过克隆创建对象结构型适配器让不兼容接口合作结构型桥接抽象与实现解耦各自变化结构型组合树形结构中统一处理叶子与容器结构型外观为复杂子系统提供统一入口结构型代理控制访问代替目标对象做事结构型装饰器动态增强对象能力结构型享元共享对象减少内存消耗行为型责任链请求沿链传递直到有人处理行为型命令把请求封装成一个对象行为型解释器定义语法规则并解释语句行为型迭代器统一遍历方式不暴露内部结构行为型中介者对象之间不直接交互通过中介行为型备忘录保存和恢复对象状态行为型观察者状态变化时通知所有订阅者行为型状态状态改变时行为随之改变行为型策略算法族可互换动态替换行为型模板方法骨架固定子类实现可变步骤行为型访问者在不改变类结构的前提下追加操作我的建议是不要一上来就试图理解全部23个模式。先把表里标粗的这几个高频模式搞懂其余的混个脸熟等遇到真实场景再回来细看。软考的选择题和案例题重点考察的就是单例、工厂、策略、观察者、模板方法、装饰器、适配器这几个。3.2 面试题怎么答不翻车面试里设计模式几乎是必问但问法各不相同。我把被问过频率最高的问题列一下附上我的回答思路第一个问题单例模式有几种写法双重检查锁为什么加volatile这个问题要答两层。第一层把饿汉式、懒汉式、双重检查锁、静态内部类、枚举这几种写法说全。第二层解释DCL为什么需要volatile因为new不是原子操作指令可能重排序volatile禁止重排序确保其他线程不会拿到半初始化的对象。能答到第二层基本就能过。第二个问题策略模式和状态模式有什么区别这个很容易搞混。我的理解是策略模式的选择方是客户端你想用哪个算法自己决定运行时替换是自由的状态模式的选择方是对象自身当内部状态改变时行为跟着自动变客户端控制不了。一个是“我选个算法”一个是“我的状态决定我的行为”。第三个问题工厂模式和抽象工厂模式有什么区别工厂方法创建的是一个产品对象由子类决定具体类型抽象工厂创建的是一个产品族保证多个产品之间的配套关系。举个例子工厂方法像你只买一个手机至于买苹果还是安卓你说了算抽象工厂像你要配一整套设备买了苹果手机就得配苹果耳机、苹果手表不能让安卓手机和苹果手表凑在一起不兼容。第四个问题你项目里哪里用过设计模式这是最关键的实操题答不好就露馅。我的答题套路是先讲场景再讲痛点最后讲方案。比如“我在支付模块遇到过if-else膨胀的问题每加一个支付渠道就要改一坨代码后来用工厂模式负责创建支付对象用策略模式封装支付算法新增渠道只需要新增一个策略类并注册业务代码完全不用改”。面试官要听的不是定义而是你到底会不会用。3.3 什么时候不要用设计模式这块我要多说几句因为现在网上很多教程把设计模式说成了“包治百病”但实际上不是所有代码都适合套模式。我见过最离谱的代码是为了用工厂模式把一个只有两种对象的创建过程也拆成接口、实现类、工厂一堆文件最后改起来反而更费力。我现在的判断标准很简单如果一段逻辑只有两三个分支且未来不会频繁扩展直接写if-else别硬套。如果需求本身非常稳定不会变任何设计模式都是在浪费复杂度。如果一个模式让代码变得更难懂而不是更容易懂那一定是用错了地方。设计模式是重构出来的不是预先设计出来的。先写一版能跑的代码等发现它确实在变化点上频繁改动再引入模式。这句话我建议你记住能用简单代码解决问题就不要引入模式。模式是为了让复杂问题变简单而不是让简单问题变复杂。4. 从理论到实践学习路径与实战练手4.1 别背定义先在项目里找模式我学设计模式的转折点不是把书看完而是在Spring源码里认出了模式。我建议你也走这条路看任何一个你手头在用的框架源码先别管细节就找“模式黑话”。比如Spring的BeanFactory是工厂模式ApplicationEventPublisher加EventListener是观察者模式JdbcTemplate是模板方法模式RestTemplate也是模板方法模式Spring AOP底层是代理模式MyBatis的Configuration、SqlSessionFactory是典型的工厂加建造者Java IO的流套流是装饰器模式Collections.unmodifiableList()返回的其实是装饰器Integer.valueOf()在-128到127之间有缓存是享元的思想。当你开始用“模式视角”看源码你会发现其实你早就在用设计模式了只是不知道它的名字。这一步做完你对模式的理解就再也不会退化成“背定义”了。4.2 给新手的实战练手清单光看不练没意义我列几个低成本、见效快的练手题目每个都覆盖两三个模式练手1多功能收银台。要求支持支付宝、微信、银联三种支付方式每种方式的支付逻辑不同。用工厂模式创建支付对象用策略模式封装支付算法用单例模式管理配置信息。做完之后尝试加一种叫“数字人民币”的方式看需要改哪几个地方。如果只加一个类就能完成说明你套对了。练手2仿Spring事件机制。自己实现一个极简的事件发布订阅系统。定义EventPublisher和EventListener接口发布用户注册事件分别写短信监听器和积分监听器注册新用户时只调用publish方法验证新增监听器是否需要改动发布方代码。练手3文本过滤器。实现一个类似Java IO的装饰器链一个基础文本读取器然后给它套上“去除空白”装饰器、“转大写”装饰器、“敏感词过滤”装饰器支持任意顺序组合。做完你就会明白为什么Java IO要设计成流套流了。练手4数据导出器。用模板方法模式做一个导出流程固定“查询数据、格式化、写文件、记录日志”的流程骨架但“格式化”方式由子类实现比如CSV导出和JSON导出是两个子类。后续要加Excel导出只需要再写一个子类。每做完一个项目给自己两个问题类数量增加了多少未来加需求时改动量是变大还是变小这两个问题能帮你判断模式用得值不值。4.3 工具与资源画模式结构图这件事我建议从最早就开始。工具我用过不少最顺手的还是Draw.io免费、能画UML类图、能导出到Git仓库。StarUML也更专业适合写期末作业和软考论文时用。画图的价值不是好看而是逼你把类之间的关系理清楚谁继承谁、谁依赖谁、谁组合谁。很多模式你看着文字怎么都记不住一画图就通了。书的话入门首推《Head First 设计模式》对话风格牺牲了一点深度但非常友好。想深入一点可以看《设计模式之禅》语言通俗例子也贴近国内开发场景。原著《Design Patterns》是GoF的经典适合当工具书查不适合通读。最后再分享一个我自己的学习习惯我给自己定了条规矩在提交每个需求之前先在代码里找一个“变化点”然后问自己三个问题——哪里在变谁依赖它将来会怎么变先想第一个问题再想后两个。你会发现大多数时候根本用不上设计模式但一旦用上代码质量会明显上一个台阶。我踩过很多坑之后最大的体会就是设计模式背得再熟也不如真实项目里“被坑过后”的领悟来得深。别急着套模式先写出来、先重构、再抽象这条路才是大多数普通开发者真正能走通的路。