资讯动态

设计模式入门:一个外卖订单方法,是怎么烂成 if-else 灾难的?

发布时间:2026/10/22 13:00:01 来源:尧图企业网站定制
作者鱼宵 「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没有 bashcd C:\Users\haoji\Desktop\课程\design-patterns-journey\lesson-01 # 你的 clone 路径 $env:JAVA_HOMEC:\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.4088 × 0.8 70.4正确只发了 App 推送。订单 3 实付666.00无优惠原价正确一个通知都没发。重点不是 “结果对”而是这个结果背后每个 if-else 分支都是将来要改的地方。你现在已经用六大原则给这段代码做完第一次 “坏味道体检” 了。五、挑战题改参数会怎样答案在源码里⭐ 把订单 1 的金额从150改成80再跑一遍 —— 实付变成多少为什么 “满减” 券没生效⭐⭐ 在Main里加一个type4的订单再跑一遍 —— 控制台打印什么注意它没有抛异常这种 “静默失败” 为什么比直接报错更难查⭐⭐ 把订单 1 的优惠券从满减改成满减 后面多一个空格再跑一遍 —— 实付多少这就是魔法字符串最容易埋的坑。三题答案都在BadCodeOrderService.java里clone 下来改一行参数就能验证。跑出来的输出和你想的不一样评论区贴出来一起查。六、生产环境进阶怎么把 “体检” 用在你的代码评审上别急着学模式先学会认病。真实项目里三大头号反模式Anti-PatternGod Class上帝类啥都干字段十几个、方法几百行 —— 一个类承载了太多改变理由违反 SRP。if-else 金字塔分支层层嵌套读代码要拿尺子量缩进 —— 加一个需求就要动老逻辑违反 OCP。魔法数字 / 字符串type 1、满减这种含义靠猜的值 —— 改一个数字全项目跟着遭殃。给你一个 30 秒代码评审自检清单每次 review 过一遍检查项看到就标红方法 30 行且干多件事SRP 违反新增功能要改老 if-elseOCP 违反到处new具体类DIP 违反字段 public 裸奔LoD 违反else分支只打印不抛异常静默失败1/2/3、满减写死在代码里魔法值本系列后面每一课都是拿着这份清单给这段外卖代码开药第 3 课用工厂消灭魔法数字、第 5 课用策略消灭优惠 if-else、第 6 课用观察者消灭通知 if 连锁、第 9 课把 5 件事拆成责任链…… 你看完就会发现模式不是 “背 23 个名字”而是 “这段烂代码该用哪张处方”。七、面试回答模板Q1什么是设计模式和设计原则什么关系一句话结论原则是 “为什么”模式是 “怎么做”设计模式是前人总结的代码组织套路菜谱设计原则是套路背后的目标比如开闭原则。展开单例、工厂这些是模式它们实现的目标是 “对扩展开放、对修改关闭” 这类原则。Q2SOLID 六大原则分别是什么一句话结论S 单一职责一个类一个改变理由、O 开闭扩展开放、修改关闭、L 里氏替换子类可无缝替父类、I 接口隔离接口小而专、D 依赖倒置面向接口编程 LoD 最少知识少摸别人内部。展开能随手举一个违背的例子最有说服力 —— 比如 “一个下单方法里干了校验、算优惠、发通知三件事改优惠要动它、加通知渠道还要动它这就是违反单一职责”。Q323 种设计模式怎么分类哪些面试最高频一句话结论创建型管对象怎么造、结构型管对象怎么组合、行为型管对象怎么协作面试高频是单例 工厂 策略 ≈ 观察者 代理 / 装饰器 模板方法 责任链 建造者。展开访问者、解释器基本没人问知道名字即可。八、总结表坏味道原因解法本系列哪一课魔法数字type1/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 辅助生成

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑