上季度处理一个门店扫码点餐服务的老项目时,真正让我失眠的不是慢 SQL,也不是缓存穿透,而是一类看起来很笨的重复:同一个新菜品,上线时要同时改三个下单入口的 switch;套餐稍微换一版,又要去每个入口各改一遍组装逻辑;新建订单时 new Order(18个参数) 那种调用读起来像在破译密码。这些代码都能跑,但它们把“对象怎么被造出来”这件事藏在了业务逻辑的各个角落里,既散又重。
那轮优化做完后我回头看提交记录,发现最大的收益并不是某个中间件或算法替换,而是把创建型模式的冗余彻底收敛了。这里说的不算什么高深理论:一种创建型模式负责消灭一类“不该由业务代码来承担的对象创建重复”,五种模式刚好都落在了实际痛点附近。这篇就顺着那次重构的排查顺序,把每个模式背后真正解决的冗余问题、替换前后的差异、以及哪些地方不该硬套模式,一起拆开来讲。
1. 顺着switch找冗余:这五种重复都在“怎么把对象造出来”这一步
1.1 从坏味道入手:下单入口里的每一个散装 new
先交代一下这个服务的大致形态。门店扫码点餐有三类下单入口:用户在桌边下单的正式订单、员工提前录好的预点单、以及老用户基于历史订单发起的“再来一单”。三个入口最后都要落成订单和订单明细,但在那个老代码里,三个入口各自维护了一套从商品类型到菜品类对象的创建逻辑。
我截几段当时最有代表性的代码给你看,结构大概是这样的:
java复制// 这段代码在 OrderService 的第一个入口里出现
switch (dishType) {
case CHICKEN -> dish = new FriedChicken(spec);
case FISH -> dish = new BraisedFish(spec);
case NOODLE -> dish = new NoodleDish(spec);
// 新增菜品:这里加一个 case
}
类似的 switch,在预点单服务里有一份,在“再来一单”服务里还有一份。问题就是这么直白:当第 12 种菜品上线,你需要在三个入口里各加一个 case,在套餐组装逻辑里再加一段 if。哪怕漏改其中任意一处,就会出现“这个入口能点新菜,另一个入口点不了”的线上事故。
如果你只在代码里看一个入口,会觉得每个 switch 都没什么了不起。但把三个入口放到一起对比,重复几乎是成倍的。这就是我当时优先处理的冗余源——不是同一个方法里多写了几行,而是同一类对象创建逻辑被人为复制到了多个业务入口。业务代码被迫知道每一道菜应该怎么被 new 出来,这本身就是一种严重纠缠。
1.2 把散点归类:五类创建问题与五张重构许可证
我没有直接上手把三处 switch 合并。先把项目里所有“创建对象”的坏味道列了个清单,再对照常见创建型模式,看各自适合解决什么。这张表是当时给自己画的重构地图:
| 编号 | 冗余形态 | 典型表现 | 对应模式 |
|---|---|---|---|
| 1 | 多入口按同一类型分支创建单品 | 新增菜品要改 N 个 switch | 工厂方法 |
| 2 | 必须成套生成多个产品,版本间联动 | 套餐换版时到处改组装清单 | 抽象工厂 |
| 3 | 目标对象字段过多,构造方式冗长 | 一个订单要传十几个参数 | 建造者 |
| 4 | 同一份全局资源被反复创建 | 配置、字典、系统参数每次下单都加载 | 单例 |
| 5 | 对象创建成本高,且需要保留运行期状态 | “再来一单”每次都全量重建订单 | 原型 |
这五张“许可证”里,有些理论派会纠结第 4、第 5 个是否属于创建型,但实际做优化时我不太关心教条边界。我更关心:是否有一个创建环节被反复执行,而且这个重复对业务毫无价值。答案明明是“有”,那我就可以按对应思路去收敛。判断标准是变化频率和新需求出现时的改动成本,不是某本设计模式教材的目录顺序。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动手之前的准备:先用特征测试把旧行为固定住
2.1 用少量全链路测试兜住旧订单,别让重构变成行为漂移
设计模式重构最大的风险不是改不出来,而是改完之后业务行为悄悄变了。尤其是老项目,很多订单字段的取值逻辑是历史上一步一步“补”出来的,你读十遍源码也未必能看出一条字段为什么这样设置。
我当时做了一件很土但很管用的事:在重构前,先从 QA 环境取一批真实历史订单,把每个订单从“入口数据”到“期望的最终订单对象”之间的映射结果全部导出成 JSON,再用一个全链路测试去比对——相同的入参,改造前和改造后必须产生完全一致的订单对象。
比如我就遇到过一个坑:某个入口创建订单时,remark 字段会用“门店备注 + 用户备注 + 逗号”拼接,而另一个入口只会写入用户备注。如果直接抽公共方法,很容易把这种差异“优化”没了。有了旧行为快照,你每重构完一个点就能立刻知道行为是否被破坏,而不是等到测试环境被业务同事喊出问题。
老代码测试如果本来就缺,我也建议不要先补单元测试,而是先补一条端到端的链路测试。原因是这种重构涉及的对象创建代码散落在多个类里,单元测试得先 mock 一堆依赖,反而容易把你绕晕;端到端测试能直接验证最终产物对不对。
2.2 重画包结构,把“创建职责”隔离出业务代码
准备工作的第二件事是调整包结构。原项目的 service 包里既写业务编排,又直接 new 各种领域对象,我划线时根本分不清哪些对象是业务装配产物,哪些是创建逻辑的一部分。
我按创建职责重新画了一层,不追求教科书式分层,只是让新代码有一个可以“落位”的地方:
code复制com.example.ordering
├── domain
│ ├── dish
│ ├── combo
│ └── order
├── application
│ ├── createorder
│ ├── preselect
│ └── reorder
└── factory
├── dish
├── combo
├── orderdraft
└── config
业务入口只依赖 factory 包暴露的接口,不关心具体创建者。这样做的意义在于:以后如果出现第 13 种菜品、第 4 个下单入口,它的创建逻辑只会在某个 factory 下面出现,不会散落到任意业务类里。这一步本身不直接消减代码行数,但它决定了后面几个模式落地时不会被“旧包结构”拉回原形。
