作者:鱼宵 | 「Java 后端实战精通营」实战驱动系列・第 1 篇
完整可运行工程已开源:https://gitee.com/j67mk2/design-patterns
一、真实场景:你项目里有没有这样一个 “改哪里都怕” 的方法?
先看一段我上周在同事代码里看到的现象(业务换成了外卖订单):
需求来了:老板要加一种 “拼团单”。你打开订单服务,找到那个快 200 行的
processOrder,往里又塞了一个
else if。测试通过,上线没事。
第二周:老板要加一种 “无门槛券”。你又打开同一个方法,在优惠券的 if-else 里再加一个分支。
第三周:老板要加 “微信通知”。同一个方法,通知的 if 连锁里再多一个 if。
一个月后:这个方法的 if-else 嵌套了三层,你改优惠的时候,不小心把订单类型的分支条件动了一下,线上突然出现 “订单没处理但也没报错”—— 查了两天,最后发现是
else分支只打印了一行字,没抛异常。
你项目里有没有这样一个方法?看起来功能齐全,但每一次需求变更都要回来动它,改一处崩三处。
如果你点头了,这篇就是为你写的。我会带你做两件事:
看懂一份 “故意写烂” 的外卖订单代码,它到底烂在哪(6 处坏味道,一处一处指给你看);
学会用六大设计原则给代码做 “坏味道体检”—— 不用记 23 种模式的名字,先学会看病,模式后面慢慢开药。
代码全在design-patterns仓库的lesson-01工程里,clone 下来跑一遍,现象当场就出来。
二、核心原理:设计模式到底是什么?(先别背 23 个名字)
2.1 设计模式 vs 设计原则:一个是 “为什么”,一个是 “怎么做”
设计模式(Design Pattern):前人总结出来的、解决某一类反复出现的问题的套路,是一套 “代码该怎么组织” 的经验模板。
类比:做饭的
菜谱
。糖醋排骨有千百种做法,但 “先炸后烧、糖醋 2:1” 这种被验证过的套路,你照做就不会翻车。设计模式就是代码世界的菜谱:被无数项目验证过,照做大概率不烂。
设计原则:比模式更底层的 “为什么”。原则告诉你目标(比如 “对扩展开放、对修改关闭”),模式给你实现目标的具体招式。
一句话:原则是 “为什么”,模式是 “怎么做”。面试官问 “你平时写代码遵守哪些原则”,答的是原则;问 “手写单例”,考的是模式。
2.2 六大设计原则(本课最核心,背下来)
五个 SOLID + 一个最少知识法则(LoD)。每条我都给了 “违背的样子”,对照后面的坏代码你马上能对上号:
| 原则 | 一句话 | 生活类比 | 违背的样子 |
|---|---|---|---|
| S 单一职责 SRP | 一个类 / 方法只干一件事,只有一个改变理由 | 厨师炒菜、收银员收钱,各司其职 | 一个processOrder干了校验、算优惠、分类型、发通知、记审计 5 件事 |
| O 开闭原则 OCP | 对扩展开放、对修改关闭:加功能 = 加新代码,不是改老代码 | 插线板:加新电器插个新插头就行 | if (type == 1) … else if (type == 2)…加一种类型就要回来加分支 |
| L 里氏替换 LSP | 子类必须能无缝替换父类,行为不能变坏 | 雇了个 “助手”,结果他把活全干成了别的 | 子类重写父类方法后悄悄改参数含义,调用方以为是老行为 |
| I 接口隔离 ISP | 接口小而专,别逼实现类实现用不到的方法 | 遥控器上不该有 50 个一年用不了一次的按钮 | 一个OrderHandler接口塞了 4 个方法,某实现只用 2 个也得空实现另 2 个 |
| D 依赖倒置 DIP | 面向接口编程,别面向具体类编程 | 你点外卖用 App(抽象),不用认识每个骑手 | BadCodeOrderService service = new BadCodeOrderService(),依赖写死 |
| LoD 最少知识 | 少摸别人的内部细节,只跟 “朋友” 说话 | 你不该知道同事钱包里有多少钱 | order.orderId直接摸 public 字段,内部结构被全世界访问 |
2.3 三大分类与面试高频排序
23 种模式分三类,本质是三个问题:
创建型(5 种):对象怎么创建 —— 绕开
new的硬编码。单例、工厂方法、抽象工厂、建造者、原型结构型(7 种):类和对象怎么组合 —— 小的拼大的、适配、装饰。适配器、装饰器、代理、外观、桥接、组合、享元
行为型(11 种):对象之间怎么协作、算法怎么组织。策略、模板方法、观察者、责任链、迭代器、状态、命令、中介者、备忘录、访问者、解释器
大厂面试真实分布(高频到低频):单例 > 工厂 > 策略 ≈ 观察者 > 代理 / 装饰器 > 模板方法 > 责任链 > 建造者。访问者、解释器基本没人问,知道名字就行。
所以别再从 “23 种模式背起” 了 —— 先把高频那 8 个吃透,本系列后面 9 篇就是按这个顺序逐个实战。
三、关键代码:一个方法干 5 件事的 “坏代码样板”
本课工程就是一份故意写烂的外卖订单系统:一个订单类 + 一个把全部业务塞进一个方法的服务类。三个文件都在lesson-01/src/main/java/com/dp/lesson01/下,完整可跑,先看主角:
package com.dp.lesson01; /** * 订单对象:纯数据容器,字段全 public(本身就是坏味道,先不收拾它)。 */ public class Order { public String orderId; // 订单号 public int type; // 1=普通单 2=预售单 3=团购单(魔法数字!) public double amount; // 金额 public String couponType; // "满减"/"折扣"/"无"(字符串魔法值!) public boolean needSms; // 要不要发短信 public boolean needEmail; // 要不要发邮件 public boolean needAppPush; // 要不要发 App 推送 public Order(String orderId, int type, double amount, String couponType, boolean needSms, boolean needEmail, boolean needAppPush) { this.orderId = orderId; this.type = type; this.amount = amount; this.couponType = couponType; this.needSms = needSms; this.needEmail = needEmail; this.needAppPush = needAppPush; } @Override public String toString() { return String.format("订单[%s] 类型=%d 金额=%.2f 优惠券=%s", orderId, type, amount, couponType); } }接下来是主角 ——一个方法承载全部业务的现场:
package com.dp.lesson01; /** * 坏代码样板:一个"没有设计模式"的外卖订单服务。 * 每一处坏味道都标了对应课程,后面 9 课会一节一节把它拆掉、换成真正的设计模式。 */ public class BadCodeOrderService { public void processOrder(Order order) { // ===== 坏味道 1:校验逻辑裸奔 ===== if (order.orderId == null || order.orderId.isEmpty()) { throw new IllegalArgumentException("订单号不能为空"); } // ===== 坏味道 2:魔法字符串判断优惠券类型 ===== // "满减"/"折扣"写死,敲错一个字(比如"满减 "带空格)优惠就静默失效。 double finalAmount = order.amount; if ("满减".equals(order.couponType)) { if (order.amount >= 100) { // 满 100 减 20 finalAmount = order.amount - 20; } } else if ("折扣".equals(order.couponType)) { finalAmount = order.amount * 0.8; // 打 8 折 } // "无"直接走原价——逻辑散落在各个 if 里,没有一个统一出口 // ===== 坏味道 3:魔法数字判断订单类型 ===== // 每加一种订单类型,就得来这里加一个 else-if 分支(违反开闭原则) if (order.type == 1) { System.out.println("[库存] 普通单:立即扣减库存"); } else if (order.type == 2) { System.out.println("[库存] 预售单:只锁库存,不扣减"); } else if (order.type == 3) { System.out.println("[库存] 团购单:按团购人数批量扣减"); } else { // 类型不支持时居然只是打印一句话,没有抛异常——坏味道:静默失败 System.out.println("[库存] 未知订单类型:" + order.type); } // ===== 坏味道 4:发通知的 if 连锁 ===== // 每加一种通知渠道(短信、邮件、App 推送、微信),都要回来改这个方法 if (order.needSms) { System.out.println("[通知] 给用户发短信:您的订单 " + order.orderId + " 已受理"); } if (order.needEmail) { System.out.println("[通知] 给用户发邮件:订单 " + order.orderId + " 详情已生成"); } if (order.needAppPush) { System.out.println("[通知] App 推送:订单 " + order.orderId + " 有新进展"); } // ===== 坏味道 5:审计"日志"只是打印 ===== System.out.println(String.format("[审计] 订单 %s 处理完成,实付 %.2f 元", order.orderId, finalAmount)); // ===== 坏味道 6:方法结束时没有任何统计 ===== // 没有"耗时多少""成功/失败"这类可观测信息 } }最后是入口Main,跑 3 个订单把坏代码 “活着的样子” 演出来:
package com.dp.lesson01; public class Main { public static void main(String[] args) { System.out.println("========== 第 1 课:坏代码样板运行 =========="); // 直接 new 一个服务:调用方和具体实现绑死(违反依赖倒置) BadCodeOrderService service = new BadCodeOrderService(); // 订单 1:普通单 + 满减券,短信+邮件都要发 Order order1 = new Order("A1001", 1, 150, "满减", true, true, false); System.out.println("【订单 1】" + order1); service.processOrder(order1); System.out.println(); // 订单 2:预售单 + 折扣券,只发 App 推送 Order order2 = new Order("A1002", 2, 88, "折扣", false, false, true); System.out.println("【订单 2】" + order2); service.processOrder(order2); System.out.println(); // 订单 3:团购单 + 无优惠券,什么通知都不发 Order order3 = new Order("A1003", 3, 666, "无", false, false, false); System.out.println("【订单 3】" + order3); service.processOrder(order3); System.out.println(); System.out.println("========== 运行结束:这段代码能跑,但每一次业务变化它都会变成灾难 =========="); } }这段代码讲了什么:校验 → 算优惠 → 按类型分发 → 发通知 → 记审计,5 件事全堆在一个方法里。看起来 “功能齐全”,但对照 2.2 的表格你数一数 —— 魔法数字(可读性)、一个方法 5 件事(SRP)、加需求改老代码(OCP)、直接 new 依赖写死(DIP)、字段 public 裸奔(LoD)、静默失败(健壮性),六处坏味道一个不少。
四、完整演示 + 实测输出
clone 下来后,在lesson-01目录执行(Windows + PowerShell,没有 bash):
cd C:\Users\haoji\Desktop\课程\design-patterns-journey\lesson-01 # 你的 clone 路径 $env:JAVA_HOME="C:\jdk\jdk-17.0.12" # 用户级 JAVA_HOME 指向 JDK8,必须临时切到 17 mvn clean install -DskipTests编译通过后运行(用 JDK17 全路径,PATH 上的 java 可能是 JDK8 会报 UnsupportedClassVersionError):
& "C:\jdk\jdk-17.0.12\bin\java.exe" -cp ".\target\classes" com.dp.lesson01.Main实测输出(本机真实跑出来的):
========== 第 1 课:坏代码样板运行 ========== 【订单 1】订单[A1001] 类型=1 金额=150.00 优惠券=满减 [库存] 普通单:立即扣减库存 [通知] 给用户发短信:您的订单 A1001 已受理 [通知] 给用户发邮件:订单 A1001 详情已生成 [审计] 订单 A1001 处理完成,实付 130.00 元 【订单 2】订单[A1002] 类型=2 金额=88.00 优惠券=折扣 [库存] 预售单:只锁库存,不扣减 [通知] App 推送:订单 A1002 有新进展 [审计] 订单 A1002 处理完成,实付 70.40 元 【订单 3】订单[A1003] 类型=3 金额=666.00 优惠券=无 [库存] 团购单:按团购人数批量扣减 [审计] 订单 A1003 处理完成,实付 666.00 元 ========== 运行结束:这段代码能跑,但每一次业务变化它都会变成灾难 ==========输出解读:
订单 1 实付
130.00:满 150 减 20,正确;短信 + 邮件都发了(两个needXxx都是 true)。订单 2 实付
70.40:88 × 0.8 = 70.4,正确;只发了 App 推送。订单 3 实付
666.00:无优惠原价,正确;一个通知都没发。
重点不是 “结果对”,而是这个结果背后每个 if-else 分支都是将来要改的地方。你现在已经用六大原则给这段代码做完第一次 “坏味道体检” 了。
五、挑战题(改参数会怎样?答案在源码里)
⭐ 把订单 1 的金额从
150改成80,再跑一遍 —— 实付变成多少?为什么 “满减” 券没生效?⭐⭐ 在
Main里加一个type=4的订单,再跑一遍 —— 控制台打印什么?注意:它没有抛异常,这种 “静默失败” 为什么比直接报错更难查?⭐⭐ 把订单 1 的优惠券从
"满减"改成"满减 "(后面多一个空格),再跑一遍 —— 实付多少?这就是魔法字符串最容易埋的坑。
三题答案都在BadCodeOrderService.java里,clone 下来改一行参数就能验证。跑出来的输出和你想的不一样?评论区贴出来一起查。
六、生产环境进阶:怎么把 “体检” 用在你的代码评审上
别急着学模式,先学会认病。真实项目里三大头号反模式(Anti-Pattern):
God Class(上帝类):啥都干,字段十几个、方法几百行 —— 一个类承载了太多改变理由(违反 SRP)。
if-else 金字塔:分支层层嵌套,读代码要拿尺子量缩进 —— 加一个需求就要动老逻辑(违反 OCP)。
魔法数字 / 字符串:
type == 1、"满减"这种含义靠猜的值 —— 改一个数字全项目跟着遭殃。
给你一个 30 秒代码评审自检清单(每次 review 过一遍):
| 检查项 | 看到就标红 |
|---|---|
| 方法 > 30 行且干多件事 | SRP 违反 |
| 新增功能要改老 if-else | OCP 违反 |
到处new具体类 | DIP 违反 |
| 字段 public 裸奔 | LoD 违反 |
else分支只打印不抛异常 | 静默失败 |
1/2/3、"满减"写死在代码里 | 魔法值 |
本系列后面每一课,都是拿着这份清单给这段外卖代码开药:第 3 课用工厂消灭魔法数字、第 5 课用策略消灭优惠 if-else、第 6 课用观察者消灭通知 if 连锁、第 9 课把 5 件事拆成责任链…… 你看完就会发现,模式不是 “背 23 个名字”,而是 “这段烂代码该用哪张处方”。
七、面试回答模板
Q1:什么是设计模式?和设计原则什么关系?
一句话结论:
原则是 “为什么”,模式是 “怎么做”
;设计模式是前人总结的代码组织套路(菜谱),设计原则是套路背后的目标(比如开闭原则)。展开:单例、工厂这些是模式,它们实现的目标是 “对扩展开放、对修改关闭” 这类原则。
Q2:SOLID 六大原则分别是什么?
一句话结论:
S 单一职责(一个类一个改变理由)、O 开闭(扩展开放、修改关闭)、L 里氏替换(子类可无缝替父类)、I 接口隔离(接口小而专)、D 依赖倒置(面向接口编程)+ LoD 最少知识(少摸别人内部)
。展开:能随手举一个违背的例子最有说服力 —— 比如 “一个下单方法里干了校验、算优惠、发通知三件事,改优惠要动它、加通知渠道还要动它,这就是违反单一职责”。
Q3:23 种设计模式怎么分类?哪些面试最高频?
一句话结论:
创建型管对象怎么造、结构型管对象怎么组合、行为型管对象怎么协作
;面试高频是
单例 > 工厂 > 策略 ≈ 观察者 > 代理 / 装饰器 > 模板方法 > 责任链 > 建造者
。展开:访问者、解释器基本没人问,知道名字即可。
八、总结表
| 坏味道 | 原因 | 解法(本系列哪一课) |
|---|---|---|
魔法数字type=1/2/3、魔法字符串"满减"/"折扣" | 值写死,含义靠猜,改一处全项目遭殃 | 第 3 课工厂 / 第 5 课策略 |
| 一个方法干 5 件事 | 5 个改变理由,改哪都怕 | 第 9 课责任链组合实战 |
| 加一种优惠 / 渠道就改老方法 | 违反开闭原则,风险跟着来 | 第 5 课策略 / 第 6 课观察者 |
调用方直接new具体类 | 违反依赖倒置,实现换不了 | 第 8 课代理 |
| 字段 public 裸奔、静默失败不报错 | 违反最少知识、健壮性差 | 第 10 课企业级收官 |
九、关于这个系列
本文是「Java 后端实战精通营」设计模式系列第 1 篇。这个系列不按 23 种模式教科书式罗列,只打三张牌:面试高频模式 + Spring/MyBatis 源码真实落点 + 坏代码实战重构—— 你现在看到的这份外卖烂代码,就是后面 9 篇的 “手术对象”。
完整可运行工程:https://gitee.com/j67mk2/design-patterns(本课源码在
lesson-01/,主类com.dp.lesson01.Main)下一篇预告:《单例模式 —— 一个对象,全宇宙只有一个》。面试出现率第一的模式,五种写法全跑一遍,再亲手演示反射和序列化怎么破坏单例、枚举为什么是唯一安全写法,顺便讲清 “Spring 单例 Bean 和单例模式什么关系”。
互动提问:你项目里最烂的那个方法叫什么?它违反了哪条原则?评论区聊聊,我帮你 “开药方”。
本文部分内容由 AI 辅助生成