上季度处理一个门店扫码点餐服务的老项目时,真正让我失眠的不是慢 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 下面出现,不会散落到任意业务类里。这一步本身不直接消减代码行数,但它决定了后面几个模式落地时不会被“旧包结构”拉回原形。
3. 工厂方法消化菜品创建逻辑:把重复的 switch 收成一个注册点
3.1 重构前的每个创建现场都有重复的 switch
先用最典型的“菜品创建”动手。旧代码里,三个下单入口各自维护了一段“根据 dishType 生成菜品对象”的 switch。我当时把三端的分支代码并列放着看,除了 case 的菜品种类基本一致外,唯一区别是每处的上下文不同。
以“藤椒鸡”这个新增菜品为例,在旧结构里完整上线,需要操作的位置是这样的:
- 扫码下单入口的
switch加一个case。 - 预点单入口的
switch加一个case。 - “再来一单”入口的
switch加一个case。 - 套餐组装处再加一段
if (isNewCombo) ...。
四个位置,四个改动点。如果哪次上线漏了第 3 个入口,老用户点“再来一单”时就会拿到一个不支持的类型,然后系统默默失败。这种问题很难被常规 CR 发现,因为 review 往往只关注你改了哪里,不太会想到你没改哪里。
3.2 引入菜品工厂注册表,让新增菜品只有一个改点
清理冗余的第一步,我给每一类菜品建立一个独立的 DishFactory,对象创建不再散落在业务代码里,而是由对应工厂负责。再通过一个注册表把“类型”和“工厂”绑定起来,业务侧只对着注册表取工厂、要产品:
java复制public interface DishFactory {
Dish create(DishSpec spec);
}
public class FriedChickenFactory implements DishFactory {
@Override
public Dish create(DishSpec spec) {
return new FriedChicken(spec);
}
}
public class BraisedFishFactory implements DishFactory {
@Override
public Dish create(DishSpec spec) {
return new BraisedFish(spec);
}
}
注册表和入口封装,大致长这样:
java复制public final class DishFactoryRegistry {
private static final Map<DishType, DishFactory> FACTORY_MAP = new HashMap<>();
static {
register(DishType.CHICKEN, new FriedChickenFactory());
register(DishType.FISH, new BraisedFishFactory());
register(DishType.NOODLE, new NoodleDishFactory());
// 新增菜品:在这里补一行
}
public static void register(DishType type, DishFactory factory) {
FACTORY_MAP.put(type, factory);
}
public static DishFactory get(DishType type) {
DishFactory factory = FACTORY_MAP.get(type);
if (factory == null) {
throw new UnsupportedOperationException("Unsupported dish type: " + type);
}
return factory;
}
}
业务入口从以前写 switch,变成一行调用:
java复制Dish dish = DishFactoryRegistry.get(spec.getType()).create(spec);
新菜“藤椒鸡”上线时,唯一的代码改动就是注册表里的 register 一行。三个下单入口全部不需要动,因为它们不再关心某个具体菜品如何 new,只关心“给我这个类型的菜品”。
当然,我也知道有些人会指出:严格按 GoF 定义,DishFactoryRegistry 这种形态更像“简单工厂 + 注册表”,而“工厂方法”强调的是通过子类或继承让工厂决定创建哪个对象。实际工程里,只要工厂接口存在、每种产品对应一个工厂实现,创建逻辑就已经被收敛到方法级了。至于是否要写一堆子类去覆盖 createXxx(),看项目继承体系复杂度决定,不必非按理论原教旨走。这正是设计模式落地的现实状态。
3.3 与策略模式的取舍:选错模式的常见信号
同类的 switch 经常被人推荐用“策略模式”来重构。但在这次场景里我并没有选策略模式,因为它们的关注点根本不同:
- 策略模式解决的是行为算法可替换的问题,比如不同的促销计算规则,输入输出结构一致但算法不同。
- 工厂方法解决的是对象如何创建的问题,重点是把构造过程从业务中抽离。
如果用策略模式去处理“创建菜品”,你会得到一堆“菜品创建策略”,它们的行为和原来 switch 里的 case 一一对应,最后你还是得有一个 map 去选择策略。这不过是在旧的类型分支外面再套一层策略壳,没有真正把“创建职责”抽出来。而且对于创建过程较复杂、需要装配私有字段的产品,策略模式的算法语义完全对不上。
所以我的判断标准很简单:如果一个分支里做的事是“new 一个对象并塞参数”,那就是创建逻辑,优先考虑工厂方法;如果一个分支里做的事是“用一套算法计算一个结果”,才值得考虑策略模式。选错模式的代价不会立刻暴露,但后续你会不断被迫对这个类打补丁,最终它还是会变质。
4. 抽象工厂接手“按套餐成套生产”,消除组装清单越长越乱的问题
4.1 套餐不是单个对象,而是一组同版产品族
菜品单品做完之后,我开始处理套餐组装。这个点很容易被忽略,因为套餐在数据库模型里往往也是“明细行”而已,看起来不过是几张表的关系。但业务上不是这么简单:一个套餐要同时包含主品、小食、饮品三类,而且不同套餐版本里,这三类的组合是强绑定的。比如“单人超值餐”和“双人分享餐”,不只价格不同,连主品用什么、小食给什么、饮品选哪个,都是成套变化的。
旧代码的问题在于,套餐的组装逻辑没有单独抽象,而是被塞在三个下单入口里的 if-else 中:
java复制if (comboType == SINGLE) {
items.add(new FriedChicken(spec));
items.add(new Fries(spec));
items.add(new Cola(spec));
} else if (comboType == DOUBLE) {
items.add(new BraisedFish(spec));
items.add(new Rice(spec));
items.add(new LemonTea(spec));
}
当套餐版本变多时,每个入口的 if-else 会越来越长。而且因为主品、小食、饮品分别来自不同的类,你很难用一个简单的对象去替代这段逻辑。这时候就轮到抽象工厂上场。
4.2 用版本化组装的抽象工厂替代三处自由 new
简单说,抽象工厂就是“针对一组相关产品,提供一套创建接口”。套餐对这个模型的匹配度很高:套餐就是一个产品族,你不需要单独问“这个套餐的主品是什么”再自己去 new,而是直接从一个套餐工厂里把整套拿走。
我先定义一个接口:
java复制public interface ComboFactory {
MainDish createMain();
Snack createSnack();
Drink createDrink();
}
然后为不同套餐版本分别实现这个接口:
java复制public class SingleComboFactory implements ComboFactory {
@Override
public MainDish createMain() {
return DishFactoryRegistry.get(DishType.CHICKEN).create(...);
}
@Override
public Snack createSnack() {
return new Fries(...);
}
@Override
public Drink createDrink() {
return new Cola(...);
}
}
public class DoubleComboFactory implements ComboFactory {
@Override
public MainDish createMain() {
return DishFactoryRegistry.get(DishType.FISH).create(...);
}
@Override
public Snack createSnack() {
return new Rice(...);
}
@Override
public Drink createDrink() {
return new LemonTea(...);
}
}
下单入口的组装逻辑变成得很干净:
java复制ComboFactory comboFactory = ComboFactoryRegistry.get(comboType);
items.add(comboFactory.createMain());
items.add(comboFactory.createSnack());
items.add(comboFactory.createDrink());
以后套餐换版,或者要新增一个家庭套餐,只需要新增一个 ComboFactory 实现类,并在注册处登记这个版本对应的类型,三个入口不需要因为套餐搭配的调整再动任何代码。
很多新人会问:这不就是把 if-else 里的 new 挪到另一个类里了吗?区别就在这里:旧代码里三个入口各自维护同样的 if-else,而新代码里产品族之间的组合关系只存在于 ComboFactory 的实现类内部。如果你要改“单人超值餐的小食从薯条换成鸡块”,你只需要改 SingleComboFactory 这一个类。套餐组装这件事,从“业务入口重复决定”变成了“由专门工厂决定”。
4.3 套餐工厂里如何让其他创建型模式合作
这里有一个很自然的联动点:SingleComboFactory 在创建主品时,调用的正是前面工厂方法里实现的 DishFactoryRegistry。也就是说,抽象工厂负责决定这一族产品的搭配,而最终单个产品如何创建,仍然交给单品工厂去完成。
这样分层之后,你改“藤椒鸡”这种单品时,影响面不会波及套餐工厂;你改套餐搭配时,也不用手动去三个入口调整单品顺序。产品本身的变化和产品组合的变化被拆到两个层面,不会互相牵连。
结合实操经验我要提醒一点:抽象工厂不要一上来就铺得太大。只有当你确认这里的“族”确实是一起变化的,比如版本升级时主品、小食、饮品必须同步换,才去建立这样一个工厂接口。如果菜品和小食在业务上本来就可以任意交叉,你给它建一个抽象工厂,反而会制造虚假约束,后面每个不匹配的组合都要在工厂里写特殊判断。
5. 建造者重构订单对象本身,把“堆积构造函数”改成分步装配
5.1 Order 被写成了十几个字段的构造入口
处理完菜品和套餐,剩下的主战场是订单对象本身。这个对象在旧代码里基本是构造函数大聚会,我见过它最夸张的写法是这样:
java复制Order order = new Order(
userId,
shopId,
totalAmount,
discountAmount,
address,
deliveryType,
expectTime,
couponId,
remark,
createBy,
source,
null,
0,
...
);
一段真实的调用里甚至能排到十几个参数。每次新增字段,IDE 编译报错你能立刻看到;但你很难分清哪个参数是“配送地址”、哪个参数是“门店备注”,因为同一个位置传错值的 bug 很容易发生,而编译器根本帮不了你。业务上很多时候,不同下单入口使用到的订单字段并不一致:扫码下单需要配送字段,预点单可能只有门店基本信息,部分入口压根不需要优惠券。
我当时的处理办法是把订单对象的分步装配过程抽出来,让各入口用语义明确的分组方式构建:
java复制Order order = Order.newBuilder()
.basic(userId, shopId)
.addItem(dishItem1)
.addItem(dishItem2)
.delivery(address, deliveryType, expectTime)
.promotion(couponId)
.remark(remark)
.build();
这里的每一步,都对应订单对象中一个明确的业务分段。你不需要关心 Order 内部那 18 个字段最终怎么初始化,只需要按“基础信息、明细、配送、优惠、备注”的顺序一步步把东西塞进去。
5.2 把三种录入场景映射成可选择的链式语义
订单对象之所以适合建造者,是因为它有明显的“分步装配”特征。三类的下单入口对于同样的订单结构,选择的字段不总是一致。
我在建造者中顺势定义了三种便捷入口:fromScanCode()、fromPreOrder()、fromReorder()。每一种都生成一个预设好了该场景必填字段的 builder,然后各个入口在此基础上继续拼接自己需要的部分。
比如扫码下单的场景,必须在 builder 基础上设置配送信息;而预点单不需要用户地址,它就不需要调用 delivery(),builder 的默认值会处理缺失字段,而不是强迫调用方传 null。
把构造函数改成带语义的步骤后,代码的识别度明显提升。你不再需要去翻 Order 类的 18 个字段来找哪个参数是备注,直接看 .remark() 方法名就够了。
5.3 建造者和抽象工厂各自负责的层面要做区分
我看到不少团队会在这个节点上把建造者和抽象工厂搞混。因为它们都解决“多个对象怎么组织”的问题,但边界其实很清楚:抽象工厂组装的是一个“产品族”,它生产的是由不同产品对象组合成的一个套装;建造者组装的是一个“复杂对象”,它只关心把一个对象的内部状态逐步构建完整。
用这个项目来打比方:
- 抽象工厂创建的是套餐,它由主品、小食、饮品三个不同对象组成,最后返回的是一个列表或一个“套餐集合”。
- 建造者创建的是订单,尽管订单内部也有明细列表,但明细的多少、排列与订单本身的聚合关系绑定得非常紧,它不是“族”的概念,而是一个完整对象内部的分步构造。
如果你把订单也用抽象工厂去写,你得定义一个“订单工厂”来生成用户、地址、优惠券、明细……结果就是工厂要依赖一堆不相关的对象构造器,违背了“一起变化才一起封装”的初衷。所以,先判断你创建的目标是“多个对象组成的族”,还是“一个对象的复杂结构”,选型就错不了。
6. 单例与原型补齐特殊场景:共享配置与重复订单复制
6.1 配置这类真的“全局唯一”的实例,每单重建是无意义开销
配置资源重复创建的问题在排队高峰期尤为明显。旧代码里,门店区域配置、配送时段配置、系统参数等对象在下单链路里多次被 new。当时看下来,最典型的是一个 ShopConfigLoader:每个下单入口为了拿到覆盖区域规则,都先 new 一个 loader 再去加载。一个入口在处理高峰期每秒几十单时,同一份配置就可能被加载几十次。
但单例模式在这个项目里并不是拿来“逞酷”的,它需要很克制的适用范围。我把它限制在“全局唯一的只读配置”上,并且采用启动期加载、运行期只读的方式,避免单例保存可变状态导致跨请求污染:
java复制public final class ConfigCenter {
private static volatile ConfigCenter instance;
private final ShopConfig shopConfig;
private ConfigCenter() {
this.shopConfig = loadShopConfigFromRemote();
}
public static ConfigCenter getInstance() {
if (instance == null) {
synchronized (ConfigCenter.class) {
if (instance == null) {
instance = new ConfigCenter();
}
}
}
return instance;
}
public ShopConfig getShopConfig() {
return shopConfig;
}
}
之所以这里用双重检查锁,而不是简单饿汉式,是因为初始化方法里包含远程加载配置这类耗时逻辑,我不想在类加载阶段就触发高成本 IO。用 volatile 防止多线程下看到半初始化的实例。
正经做项目时,我也建议优先让 Spring 这类容器管理单例 Bean,让容器保证生命周期,不一定要自己手写单例锁。但有些老工程里工具类、非容器组件确实没有托管条件,手写单例也无可厚非。关键是别把订单、购物车这类本来就因用户而异的可变状态放进单例里,否则你会上线后看到各种串数据的诡异问题。
6.2 “再来一单”用原型复制订单草稿,避免全量重查库再装配
原型模式解决的是“基于已有对象复制”的高成本创建问题。它在这个项目里的真实落点是“再来一单”。
旧逻辑对“再来一单”的实现方式很直接:用户点击后,按历史订单 ID 去库里把旧订单明细一行行查出来,再在内存中根据菜品 ID 逐个重新创建对象、重新装配一个订单。高峰期用户反复下单时,这段“重查库 → 重装配”对数据库和 Java 堆都是额外压力,而这对于高重复度的订单尤其不划算。
优化后的思路是:内存里维护一份“订单草稿原型”,每当新订单只是在上一次订单基础上修改少量字段时,直接复制该原型,再对需要差异的部分做修改:
java复制public class OrderDraft implements Cloneable {
private String userId;
private List<OrderItemDraft> items;
private Address address;
private List<PromotionSnapshot> promotions;
@Override
public OrderDraft clone() {
try {
OrderDraft clone = (OrderDraft) super.clone();
// 浅拷贝只复制了引用,列表必须深拷贝
clone.items = items.stream()
.map(OrderItemDraft::clone)
.collect(Collectors.toList());
clone.promotions = new ArrayList<>(promotions);
return clone;
} catch (CloneNotSupportedException e) {
throw new IllegalStateException("订单草图克隆失败", e);
}
}
}
业务侧里,用户点“再来一单”时直接拿最近一次成功订单的快照原型做克隆,然后只更新数量、地址、优惠券这些差异字段,省掉了全量重查数据库再逐条装配的过程。
这段代码真正值得注意的地方在深拷贝:如果不把 items 列表里的每个元素单独 clone 一遍,只复制列表引用,那么两个订单草稿会共享同一批明细对象。如果你后来修改其中一个草稿的明细数量,另一个草稿也会被“传染”。在这个项目里,促销快照、明细项都是可变对象,所以我都对它们做了深复制;地址这类不可变对象则可以放心复用引用。
6.3 不要在工厂方法里顺手把单例和原型都做了
这里说一个我踩过的坑。有同事接手后觉得“每次从工厂里取对象太浪费了,干脆在工厂的 get 方法里加个缓存,同一个类型只创建一次”。这个想法听起来像同时优化了工厂和单例,但实际运行后发现:两个不同用户点了同一道菜,因为菜品对象被缓存,其中一个订单改菜品的辣度配置时,另一个订单的菜品也跟着变了。原因很简单——那道菜在业务里是订单明细的可变对象,并不适合做成全局共享。
设计模式单独拿出来用都不会有大问题,最怕把不同模式随意组合。工厂方法负责按需创建新对象,单例负责全局共享,原型负责基于已有对象复制,它们的职责边界在优化时不能被绕过去。如果非要用单例管理菜品对象,那菜品对象本身必须是不可变的或者在业务上就是共享引用类型,否则你只是在给未来的 bug 埋雷。
7. 冗余消除复盘:真收益是变化点收敛,不是行数减少
7.1 改动开关的比较:同一个新需求要触碰的位置
重构告一段落后,我拿“上线一个新菜品”和“上线一个新套餐组合”这两个典型需求去数改动点,对比非常直观:
| 场景 | 优化前改动点 | 优化后改动点 |
|---|---|---|
| 新增一个菜品 | 三个入口各改 switch,共 3 处;套餐组装处再加 if | 注册表补 1 行,新增 1 个工厂实现类 |
| 修改某套餐的小食搭配 | 每个下单入口的套餐 if-else 各改一次 | 只改对应的 ComboFactory 实现类 |
| 订单对象新增基础字段 | 所有调用处和构造函数都要检查 | 改 Order 内部加字段,再在 Builder 里补一步 |
| 重复订单草稿克隆 | 每次都全量查库再重建 | 直接 clone 原草稿并高亮差异字段 |
这个表格里最关键的指标不是“代码行数少了几行”,而是“改一个需求时需要触碰的位置数量”。维护老项目时,真正的维护成本是“盯着所有该改的地方别漏”,而不是打那几行代码本身。
7.2 不要为用模式而用模式:你的仓库里何时不需要这些模式
我见过很多团队把创建型模式当成“代码洁癖”,给一个只有一个实现类的接口也加工厂;给只有两三个字段的对象硬加 Builder。这种模式堆砌会引入明显的副作用:类数量激增、层级变深、新手同事读代码的负担增加。
我的判断阈值很简单:
- 如果某类对象只在代码里一个位置被创建,且未来半年内大概率不会出现第二个入口,不要先给它写工厂,保持直接 new。
- 当第二个真实需求出现时,再回头看第一个位置和第二位置之间的重复,那时候抽取工厂的收益才真正可计算。
- 字段数量如果不超过五六个,用构造函数并不会造成明显负担;一旦超过七八个,且存在多种必填可选组合时,Builder 的收益才会出现。
- 单例必须满足“全局唯一 + 只读 + 创建成本高或需要跨处共享”这三个条件;如果只是图省事,让一个 Service 变成单例保存状态,那就是给自己挖坑。
所谓“以需求为驱动”不是一句口号。无病呻吟地先把所有模式套上,意味着你已经为可能永远不会出现的第二个需求付了钱,这对冗余消除来说是反向操作。
7.3 模式重构要分阶段提交,不要五个模式一次大爆炸
最后再分享一个实操层面的经验。五个创建型模式虽然都在一个项目里落地了,但我并没有把它们放进同一个提交里。实际操作分成了三个阶段:
第一个阶段先完成菜品工厂方法,单独跑一轮冒烟测试,确认所有菜品入口行为不变。第二阶段处理抽象工厂和建造者,因为这两个改动会牵动订单对象的字段组织,需要更谨慎。第三阶段才引入单例和原型,因为它们涉及共享资源和深拷贝问题,要观察一段线上行为才能放心。
这种节奏的好处是:出了问题,你能在很小的 diff 范围内定位。如果五个模式一把梭,你会面临一个问题——线上如果一个订单字段不对,很难判断是工厂方法的问题、建造者的问题,还是原型深拷贝的问题。拆细提交其实是在降低你的“返工成本”。
重构结束以后,这个点餐服务再上线新品类变得很轻:新增一个菜,在注册表里登记一条记录,建好新菜品自己的工厂实现,跑一轮链路测试就可以发布。这和我第一次接手时看到的“上线小心得不敢动,动则四处开花”的状态,已经是两个世界。经历过这一轮你也会体会到,创建型模式的核心从来不是让你显得懂很多,而是让代码真正能把“变化”关进一个笼子里,而不是让变化散落在每个入口。
