
设计模式这个词几乎每个学编程的人都听过但直到今天我逛技术社区还能看到不少人在问“21种设计模式怎么记”“期末大作业不知道该用哪种模式”“照着书敲了代码却发现根本用不上”。作为一个用设计模式写过大型项目、也熬过期末作业的人我想把这些实际体会整理出来聊点真正有用的东西而不是把教科书里的概念再复述一遍。这篇内容不挑语言示例我会用Java来写因为Java里面向对象特性最完整面试和课程设计也都绕不开它。不管你是正在学设计模式的在校生、要交期末大作业的本科党还是已经工作两三年但发现代码越写越僵的开发者这篇文章都能帮你把“模式”这种抽象的东西落回真实的代码里。1. 先搞清楚设计模式解决的是什么问题1.1 从一段“能跑但很痛苦”的代码说起我见过太多同学交上来的大作业功能全都能跑但代码就是一个几百行的上帝类所有逻辑堆在主方法里一个类里有十个方法方法里到处是if-else改一个功能得连带看五百行代码。这种代码有个特点写的时候很爽改的时候想哭。设计模式解决的不是“代码能不能跑”的问题而是“当需求变化的时候你的代码需要改几处”的问题。举个例子。你写一个支付模块一开始只有微信支付于是你写public class PayService { public void pay(String type) { if (wechat.equals(type)) { // 微信支付逻辑 } } }产品第二天告诉你要加支付宝。你加一个else if。第三天要加银联你再加一个else if。每次新增都改动已有代码测试要回归线上还担惊受怕。这个就是典型的“坏味道”也是设计模式最核心的用武之地让系统对扩展开放、对修改关闭也就是常说的开闭原则。设计模式是前辈们从无数项目里提炼出来的“针对特定场景的标准解决方案模板”它不是语法不是框架而是一种经验结晶。第一次接触会觉得抽象但只要结合真实需求变化去体会你就能感受到它存在的价值。1.2 23种模式背后的三分类思想刚接触设计模式的人看到23这个数字会本能地发怵。但其实这数字本身不重要重要的是理解它的分类逻辑因为分类逻辑就是选择模式的思考路径。GoF把23种设计模式分成了三大类分类核心问题包含模式创建型怎么创建对象更合适单例、工厂方法、抽象工厂、建造者、原型结构型怎么组合类和对象形成更大结构适配器、装饰器、代理、外观、桥接、组合、享元行为型怎么分配职责、怎么交互协作策略、观察者、模板方法、迭代器、状态、责任链、命令、访问者、中介者、备忘录、解释器我的理解方式是创建型关心的是“new”这件事能不能更科学结构型关心的是“类之间的关系”怎么组织更清晰行为型关心的是“对象之间怎么协作”才更灵活。举例来说单例模式解决“一个类只需要一个实例”的问题属于创建型装饰器模式解决“给对象动态添加功能但不破坏原有类”的问题属于结构型观察者模式解决“一个对象状态变了其他对象要怎么被通知”的问题属于行为型。拿到需求的时候你先判断问题属于哪一类再去从那一类里挑具体模式思路就顺了。2. 高频模式的Java实现精讲2.1 单例模式从饿汉到双检锁的进化单例模式是设计模式里出场率最高的一个因为它概念简单保证一个类只有一个实例并提供一个全局访问点。听起来简单实现细节却藏了很多面试考点期末考和面试都喜欢在这里挖坑。先看最省事的写法——饿汉式public class Singleton { private static final Singleton INSTANCE new Singleton(); private Singleton() {} public static Singleton getInstance() { return INSTANCE; } }这个写法的优点是不会有多线程问题类加载时就完成了实例化线程安全。缺点是如果这个类里的构造逻辑比较重比如要读配置文件、建立网络连接那在类加载阶段就要付出成本。再看懒汉式确实做到了按需创建但普通写法在多线程下有隐患public class Singleton { private static Singleton instance; private Singleton() {} public static synchronized Singleton getInstance() { if (instance null) { instance new Singleton(); } return instance; } }给方法加synchronized能解决问题但每次调用都有同步开销。实际项目里常用的是双重检查锁public class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance null) { synchronized (Singleton.class) { if (instance null) { instance new Singleton(); } } } return instance; } }这里有两个细节是面试官最爱追问的。第一个是为什么双重检查外层判断避免无谓的加锁竞争内层判断保证真正只创建一个实例。第二个是为什么用volatile因为instance new Singleton()在底层不是原子操作会经历分配内存、初始化对象、引用指向三个步骤如果没有volatile极端情况下可能出现另一个线程拿到一个“尚未完全构造”的对象。教材上还有一种静态内部类写法利用类加载机制天然实现懒加载和线程安全我个人认为它比双检锁更优雅但面试时如果你能讲清楚双检锁的原理更显功底。2.2 工厂模式把“创建对象”这件事管起来如果我只能推荐一个模式用在期末大作业里我推荐工厂模式。因为它最直观也最容易讲清楚价值。简单工厂不是GoF官方23种模式之一但它是理解工厂方法的跳板public class PaymentFactory { public static Payment create(String type) { if (wechat.equals(type)) { return new WechatPayment(); } else if (alipay.equals(type)) { return new AlipayPayment(); } throw new IllegalArgumentException(未知支付方式); } }使用方调用时不需要知道WechatPayment类具体怎么构造只需要传入类型标识。但问题也在这里新增支付方式还得改这个工厂类没完全满足开闭原则。工厂方法模式就把创建职责交给子类。先定义抽象工厂再按产品类型实现具体的工厂类public interface PaymentFactory { Payment create(); } public class WechatPaymentFactory implements PaymentFactory { Override public Payment create() { return new WechatPayment(); } }使用方持有一个工厂对象无论是哪个具体工厂调用create得到的都是统一的Payment接口。这时候新增支付方式只需要新增产品类和对应的工厂类不动已有代码。做期末大作业时工厂模式最大的价值是让你的代码“看起来像个正规系统”。举个实际例子你做一个图书馆管理系统要处理书籍的入库、借出、归还这些操作如果直接在控制器里new各种实体代码会很散。用一个工厂方法统一创建书籍、用户、借阅记录等对象整个项目结构一下就清晰了。答辩的时候你能说出来“我用了工厂方法模式来解耦对象的创建和使用”老师至少会认定你确实学懂了设计模式。2.3 观察者模式让事件驱动代码解耦观察者模式非常适合解释什么叫“代码之间的耦合”。想象一个场景用户在系统里下单成功之后要发短信通知、要更新库存、要记录日志。如果这些都写在订单服务里订单服务就变得又长又难维护。观察者模式把“下单成功”当作一个事件让各个感兴趣的模块自己去订阅这个事件。继续说回Java代码最直白的实现是这样的public interface Observer { void update(String event); } public class SmsObserver implements Observer { Override public void update(String event) { System.out.println(发送短信通知 event); } } public class StockObserver implements Observer { Override public void update(String event) { System.out.println(更新库存 event); } } public class OrderSubject { private final ListObserver observers new ArrayList(); public void addObserver(Observer observer) { observers.add(observer); } public void notifyObservers(String event) { for (Observer observer : observers) { observer.update(event); } } public void createOrder() { System.out.println(订单创建成功); notifyObservers(新订单 #001); } }这个模式最核心的价值在于订单模块不需要知道短信模块、库存模块的具体实现也不需要在订单模块里维护一份一大堆if-else的通知逻辑。将来要加一个新的动作比如发送邮件只需要再写一个EmailObserver并注册进去订单模块一行代码都不用改。这就是可扩展性。实际开发中观察者模式的应用非常广泛Spring事件监听机制底层就是这个模型。期末大作业里如果你想主动用观察者模式比较容易的设计是做一个简单聊天室或者做一个消息通知系统一个事件源多个订阅端既能体现模式又能把功能做完整。2.4 策略模式消灭刺眼的if-else链策略模式也是大作业和面试里极其常见的模式它的核心思想是把一组可互相替换的算法封装起来让调用方在运行时选择用哪个。我见过很多同学的代码里动不动就是长长的if-else策略模式就是用来干掉这种代码的。还是用支付场景。在没有策略模式的世界里价格计算是这样的public double calculatePrice(String userLevel, double amount) { if (normal.equals(userLevel)) { return amount; } else if (vip.equals(userLevel)) { return amount * 0.9; } else if (svip.equals(userLevel)) { return amount * 0.8; } return amount; }每加一种会员等级就要改这个方法。如果用策略模式public interface DiscountStrategy { double calculate(double amount); } public class NormalStrategy implements DiscountStrategy { Override public double calculate(double amount) { return amount; } } public class VipStrategy implements DiscountStrategy { Override public double calculate(double amount) { return amount * 0.9; } }然后在调用方public class PriceCalculator { private final DiscountStrategy strategy; public PriceCalculator(DiscountStrategy strategy) { this.strategy strategy; } public double calculate(double amount) { return strategy.calculate(amount); } }业务的差异被封装进了每个策略类里“选择哪个策略”的逻辑也可以交给工厂模式处理策略加工厂这两个模式组合在一起可以说是万能搭配很多大作业里都能看到这套组合拳。策略模式有个特别容易搞混的兄弟叫状态模式。一句话区分策略模式是调用方主动选择算法状态模式是对象内部状态变化后自动切换行为。大作业答辩时被老师问到两者区别能答上来就是加分项。3. 从场景反推模式实践中的选型思路3.1 识别代码里的坏味道学了模式之后最大的坑是什么就是拿到需求就想着“我该用哪个模式”。这是顺序反了。正确做法应该是先把功能写出来然后去看代码里有没有坏味道再根据坏味道选择合适的模式去重构。坏味道其实很好识别我总结了三类最常见的第一类到处new同一类对象而且创建过程很复杂。比如你要构造一个配置对象需要一堆参数参数之间还有默认值逻辑。这时候适合用建造者模式。第二类同一套操作在不同地方重复出现但某些步骤有差异。比如多类报表导出的步骤都是“查询数据-组装格式-写入文件”只有中间细节不同。这时候适合用模板方法模式把通用流程固定下来差异步骤留给子类实现。第三类类的职责太杂动不动就几百行。比如一个类既负责加载数据又负责解析数据还要负责渲染展示。这时候就该考虑用适配器、外观这些结构型模式把职责拆分或者在行为层面用策略模式做职责分离。我在实际项目里见过一个特别典型的例子。有一个消息推送模块推送前要做内容过滤、去重、组装模板推完后要记录日志。这个模块被三个业务方调用每个业务方的过滤规则和模板都不一样最早大家用if(“业务A”)和if(“业务B”)来区分后来业务C上线这块代码改成了一锅粥。后来重构时用了模板方法来稳定流程把过滤和组装模板做成抽象方法交给子类实现问题迎刃而解。这就是识别坏味道再选模式的真实过程而不是先定一个模式硬套。3.2 拆一个经典大作业商场收银系统不少同学的设计模式大作业题目都类似——“商场收银系统”“在线购物平台”“学生选课系统”。这些题目做得千篇一律但如果你真能把模式用得恰到好处就能从一堆作业里脱颖而出。我拿商场收银系统来拆解一下看看哪些环节天然适合用哪些模式。首先是商品和订单的创建。如果直接在业务代码里new商品对象一旦商品的构造方式变化所有相关代码都要动。这里适合用工厂方法模式让不同类型的商品食品、书籍、电子产品各自对应一个工厂。然后是价格计算。商场必然会遇到不同的折扣政策周末打折、会员特价、满减活动这些就是典型的策略模式应用场景。把每种计价规则封装成一个策略类商品计算价格的时候动态选择策略比在代码里堆一堆if-else要清晰得多。再来是库存和通知。商品价格变动、库存数量低于阈值要通知清楚相关模块更新界面或者提醒管理员。这里用观察者模式商品变更作为主题界面和管理员作为观察者一旦库存变化所有观察者自动感知。订单状态的流转也可以用状态模式。比如订单从“待付款”到“已付款”到“已发货”到“已完成”每一步能做什么操作不允许做什么操作不同状态模式把每种状态对应的行为封装成独立类避免在订单类里写一大堆switch。但这里我要说一句实话期末大作业能合理运用三到四个模式已经非常出色了不需要为了用满23种模式而强行设计场景那样只会让系统变得又别扭又臃肿。把三四个模式用对地方每个模式都能说出应用背景和解决的实际问题这个作业就是优秀水平。3.3 什么时候不要用设计模式说到过度设计这个问题在大作业和真实项目里都很常见。我知道很多同学学了策略模式之后恨不得所有if-else都改成策略类。这是典型的“手里拿着锤子看什么都像钉子”。设计模式本质上是一剂药对症下药才有效。如果系统里只有两三种分支逻辑十年内根本不可能再扩展那用简单的if-else就很好反而更直观。我见过一个项目为了一个固定不变的汇率换算逻辑硬是建了五个策略类等有人去读代码时找那个唯一的实现翻了半天。这种“模式化”就是人为制造复杂度。初学者最容易犯的另一个错误是把简单需求套上复杂模式。比如只有一个Logger需求非要建一个抽象工厂来生产不同级别的日志对象。日志对象本身不需要工厂直接用枚举加静态方法就能解决。判断该不该用模式有一个比较实用的标准如果你能预见到这块逻辑在三个月到半年内大概率会有新增变化、分支扩张或多种实现并存那就有必要引入模式。如果需求已经非常确定或者业务方拍着桌子说永远不会再加新类型那就怎么简单怎么写不要为了简历上多写一个“熟悉设计模式”而去制造一个本来不存在的问题。4. 大作业、期末和面试怎么准备4.1 大作业选题与代码之外的事每年期末都有同学来问我大作业怎么选题我的建议特别直接选一个你熟悉、逻辑完整、可以自然引出多个模式的小系统。比如在线购物系统、租车系统、会议室预订系统这些都是特别经典的题目网上资料多但反而容易撞车。我更推荐往自己的兴趣方向靠比如你平时打游戏就做一个游戏角色管理器你喜欢运动就做一个赛事报名与通知系统。选一个你真正理解业务逻辑的题目你才能自然地把模式放进去而不是生搬硬套。大作业打分通常不只看代码设计文档和答辩才是拉开差距的地方。设计文档里要画清楚类图和时序图重点是展示类与类之间的关系。你在文档里至少要说明三件事这个系统有哪些核心功能每个设计模式用在哪个模块用了模式之后相比不用模式有什么好处。答辩的时候老师大概率会问“你这里为什么要用观察者模式”你如果能回答“因为订单状态变化后需要通知多个模块如果用同步调用会硬编码依赖用观察者模式就无需修改订单服务就能扩展新模块”这就能体现你真的理解了设计模式的用途。写代码的时候也要注意类命名和包结构。创建型模式相关的类放在factory包里行为型放在strategy或observer包里保持整洁。代码格式统一变量命名不要出现a1、b2这种随手写的这能直接影响老师的第一印象。4.2 期末复习一张表记住23种模式期末考前突击的话逐个看23种模式的代码细节效率太低了。最好的策略是先把每个模式“用一句话说清楚是干什么的”形成一个记忆表然后针对老师重点讲过的模式去精读代码和类图。我整理过一份一句话速记表分享给大家模式一句话核心单例保证一个类全局只有一个实例工厂方法让子类决定创建哪个产品对象抽象工厂创建一组相关的产品对象建造者一步一步构建复杂对象原型通过复制已有对象创建新对象适配器让接口不兼容的类协同工作装饰器动态给对象附加职责代理控制对真实对象的访问外观为复杂子系统提供统一门面桥接将抽象与实现分离各自变化组合将对象组合成树形结构表示整体-部分层次享元共享细粒度对象节省内存策略封装一族可互换的算法观察者对象状态变化时通知依赖它的对象模板方法定义流程骨架子类实现变化步骤迭代器统一方式遍历聚合对象状态内部状态变化时改变行为责任链多个对象依次尝试处理请求命令将请求封装为对象支持撤销操作中介者用中介对象集中管理对象间交互备忘录保存并恢复对象状态访问者在不改变类结构的前提下增加操作解释器定义语言的文法并用解释器解释句子背下这张表基本就能应付“模式匹配场景”类的选择题。如果你期末考有画类图的大题建议重点练习单例、工厂方法、策略、观察者这四种的UML画法这四个是出题频率最高的。4.3 面试高频追问与常见错误设计模式几乎是Java后端岗位面试必问的一部分面试官问的方式通常不是“你背一下策略模式的定义”而是给你一个实际场景让你设计方案。比如“多个第三方支付渠道接入你会怎么设计”这就是在考察你是否能灵活运用工厂加策略的组合。回答时要先说出整体思路再讲每个模式的职责边界最后可以问面试官是否有额外的约束条件这比闷头背定义更有说服力。面试里还有一个高频坑是关于单例的线程安全问题。如果你写的是懒汉式但没有加锁面试官一定会追问你并发情况下怎么保证单例如果你的回答只停留在“加synchronized”他还会追问性能问题和volatile的作用。这两个考点我前面已经详细讲了对应的原理一定要能吃透。另一个容易暴露功底的问题是UML类图里的关系符号。泛化关系是空心三角箭头实线实现关系是空心三角箭头虚线关联是普通箭头实线依赖是普通箭头虚线组合是实心菱形聚合是空心菱形。这些符号看着细碎但画错了在考试里扣分很吃亏面试里画错也会显得基础不牢。至于常见错误我再补充三点自己见过次数最多的。一是构造方法没有私有化导致单例可以被直接new出来二是使用了静态工厂方法但内部仍然new一个全新实例等于穿了个马甲单例名存实亡三是在抽象工厂模式里把工厂的选择逻辑又写回了产品类里导致产品类依赖工厂。纠正这些错误的方法很简单写完之后自己检查一遍类与类之间的依赖方向高层的模块不要依赖底层的具体实现细节依赖关系从中倒过来基本就不会有大问题。从我带过的人和项目经验来看设计模式的学习曲线其实很不线性往往是“初学时觉得抽象、代码敲过几个之后觉得也就这么回事、等到真正负责一个需求变动频繁的模块时才发现前辈们总结的东西有多值钱”。我到现在还会经常翻一翻设计模式相关的源码实现比如Spring里的模板方法、代理模式每看一次都能有新体会。如果你现在正在为期末或者大作业头疼回到我前面说的那句话先让代码跑起来再找里边的坏味道最后选模式去重构这条路一定走得通。