← 返回题目列表

如何把大构造器或 JavaBean 重构为 Builder?

高频 困难 第 12 / 26 题 更新于 2026/07/28
建造者模式代码重构大构造器JavaBean

简化版

重构思路是先识别必填字段、可选字段、默认值和校验规则,再新增 Builder,把复杂构造过程迁移到 build(),最后逐步收敛旧构造器或 setter 的使用。重构时要注意兼容现有调用方,避免一次性破坏太多代码。

详细版

常见坏味道有两种。

第一种是大构造器:

new ExportConfig("xlsx", true, false, 1000, "UTF-8", null, true);

第二种是 JavaBean 半初始化:

ExportConfig config = new ExportConfig();
config.setFormat("xlsx");
config.setPageSize(1000);
config.setCompress(true);

重构步骤可以这样做:

  1. 梳理字段:哪些必填、哪些可选、哪些有默认值;
  2. 梳理规则:字段范围、字段组合、互斥关系;
  3. 新增 Builder,先不删除旧 API;
  4. build() 里集中校验;
  5. Product 字段尽量改成 final
  6. 对集合、数组做防御性拷贝;
  7. 将新代码迁移到 Builder;
  8. 对旧构造器和 setter 标记废弃或限制可见性。

重构后的调用:

ExportConfig config = ExportConfig.builder()
        .format("xlsx")
        .pageSize(1000)
        .compress(true)
        .build();

这样调用方更可读,对象也更不容易处于非法状态。

完整版教学

一、先识别重构信号

不是所有对象都需要 Builder。适合重构的信号通常包括:

  • 构造方法参数超过五六个;
  • 布尔参数很多,调用方看不懂含义;
  • 多个重载构造器互相调用;
  • 可选参数越来越多;
  • 对象先 new,再连续调用 setter;
  • 某些字段必须成组出现;
  • 出现“创建后马上校验”的重复代码。

比如:

public ExportConfig(String format,
                    boolean includeHeader,
                    boolean includeChart,
                    int pageSize,
                    String charset,
                    String password,
                    boolean compress) {
    // ...
}

这类构造器参数越多,越容易出现调用错误。

二、梳理字段分类

重构前不要急着写代码,先把字段分类。

字段类型处理方式
必填字段build() 中检查,或通过 Builder 构造方法强制传入
可选字段Builder 方法按需设置
默认值字段Builder 字段初始化
派生字段build() 或 Product 构造中计算
可变引用字段构造时防御性拷贝

例如导出配置:

  • format 必填;
  • pageSize 默认 1000;
  • charset 默认 UTF-8;
  • password 可选;
  • compress 可选;
  • 如果加密导出,必须有 password。

这个梳理会直接决定 Builder 的设计。

三、迁移大构造器

可以先保留旧构造器,内部委托给 Builder 或复用校验逻辑,避免一次性改爆调用方。

@Deprecated
public ExportConfig(String format, boolean includeHeader, int pageSize) {
    this(ExportConfig.builder()
            .format(format)
            .includeHeader(includeHeader)
            .pageSize(pageSize));
}

private ExportConfig(Builder builder) {
    this.format = builder.format;
    this.includeHeader = builder.includeHeader;
    this.pageSize = builder.pageSize;
}

然后逐步把业务代码迁移为:

ExportConfig.builder()
        .format("xlsx")
        .includeHeader(true)
        .pageSize(1000)
        .build();

如果项目规模大,可以分多次提交迁移:先加新能力,再迁移调用点,最后清理旧 API。

四、迁移 JavaBean 半初始化对象

JavaBean 的问题是对象创建后可能处于不完整状态:

ExportConfig config = new ExportConfig();
service.use(config); // 此时字段可能还没设置完整
config.setFormat("xlsx");

Builder 可以避免半成品泄露。把无参构造器私有化或限制可见性,让对象只能从 build() 出来。

private ExportConfig(Builder builder) {
    if (builder.format == null) {
        throw new IllegalArgumentException("format required");
    }
    this.format = builder.format;
}

如果这个类被框架反序列化、ORM、配置绑定使用,就不能贸然删无参构造器和 setter,需要看框架约束。面试时讲到这一点,会显得很工程化。

五、重构时的风险控制

重构 Builder 时要注意兼容性。

第一,不要一次性删除公共构造器。先标记 @Deprecated,给调用方迁移窗口。

第二,保持行为一致。默认值、异常类型、边界值处理都要和原逻辑对齐,除非明确要修 bug。

第三,补测试。至少覆盖必填字段缺失、默认值、生效字段、非法组合、集合拷贝等场景。

第四,注意序列化兼容。如果对象参与 JSON 反序列化,Builder 可能需要额外注解或保留无参构造器。

六、用分阶段迁移保护旧调用方和序列化契约

已有 300 个调用点的大构造器不能一步删除。先加入 Builder 和等价测试,让旧构造器委托同一校验/快照逻辑;再分批迁移调用点,最后根据兼容策略降低旧 API 可见性或在大版本移除。

阶段 1: 记录旧构造器默认值与异常
阶段 2: 新增 Builder,不删除旧 API
阶段 3: Builder 与旧构造器委托同一核心构造
阶段 4: 契约测试比较两条路径结果
阶段 5: 新代码只用 Builder
阶段 6: 批量迁移 300 个调用点
阶段 7: 旧构造器标记 @Deprecated
阶段 8: 检查 JSON/ORM 等框架反射需求
阶段 9: 版本窗口后再移除 setter/构造器
全程保持默认值和序列化字段稳定

这条时间线把“可变构建阶段”和“稳定成品阶段”分开。Builder 可以反复接收参数,但 Product 只有在所有规则通过后才出现;若 Product 在第一步就被 new 出来再逐项修改,就仍然存在半初始化对象泄漏的窗口。

七、构建契约与对象不变量

评审维度本题结论
必填信息从旧构造器语义梳理必填项,不能凭新设计猜测。
默认值迁移前后同一输入必须得到相同默认值,除非明确发布行为变更。
跨字段规则复用一处核心校验,避免旧/新入口规则漂移。
可变引用处理迁移时补齐集合防御性拷贝,但若改变可观察语义需评估兼容性。
Builder 生命周期Builder 成为新入口;旧入口按弃用计划逐步收口。
Product 交付保证迁移完成后避免半初始化,且框架绑定/序列化仍满足既定契约。

build() 不是形式上的结束标记,而是对象从“参数集合”变成“合法业务值”的原子边界。单字段输入可以提前拒绝明显错误,跨字段规则必须等信息齐全后判断,Product 私有构造器还应保留必要兜底,避免未来新增创建入口绕过不变量。

八、与构造器、JavaBean 和工厂的选择边界

  • 字段少且全部必填时,短构造器或 record 通常比 Builder 更直接。
  • 字段多、可选项多、同类型参数易错时,命名步骤能显著提升调用可读性。
  • JavaBean setter 适合某些框架绑定,但对象可能在设置完成前就被观察到。
  • Builder 关注“同一种复杂对象怎样组装”;工厂模式关注“创建哪一种产品实现”。
  • 两者可以组合:工厂选择具体产品族或 Builder,Builder 再完成复杂组装。
  • 经典 Director 只有在固定步骤序列需要复用或存在多种表示时才有价值。
  • 链式 return this 只是语法,校验、默认值、拷贝和收口才是设计语义。
  • 若 Builder 比 Product 规则还复杂,应重新拆分对象职责,而不是继续堆方法。

若对象是框架要求的可变 DTO,可保留无参构造与 setter;不要为追求纯模式破坏 ORM/序列化。

九、交付前的代码与测试检查

  1. 缺少每个必填字段分别调用 build(),应得到明确且稳定的异常。
  2. 完全不设置可选字段,核对默认值来自唯一权威位置。
  3. 传入最小值、最大值和越界值,确认范围判断没有反向或 off-by-one。
  4. 构造两个互相冲突的字段组合,确认只在信息完整时执行跨字段校验。
  5. 传入集合或数组后修改原引用,已构建 Product 不应跟着变化。
  6. 若 getter 返回可变数据,再尝试修改返回值,内部状态仍应保持不变。
  7. 连续调用同一个 Builder 两次,确认语义是明确允许复制还是文档明确禁止复用。
  8. 两线程共享一个 Builder 做压力测试应被禁止或证明安全,不能靠偶然结果。
  9. 若使用 Lombok,检查生成代码、默认值、@Singular、构造器可见性和框架兼容。
  10. 若迁移旧 API,比较默认值、异常类型、序列化字段和所有旧调用点行为。

记忆钩子:重构顺序是先兼容、再迁移、最后收口,不是先删旧构造器。

十、常见误区与追问

  • 误区:新增 Builder 后应立刻删除所有旧构造器。 公共 API 和大量调用点需要迁移窗口。
  • 误区:重构可以顺便改变默认值而无需说明。 这属于行为变更,应单独评审和测试。
  • 误区:ORM/JSON 一定能自动使用新 Builder。 不同框架需要无参构造、注解或专用 Builder 配置。
  • 追问:旧构造器如何复用新逻辑? 委托统一私有构造/校验函数,避免双份规则。
  • 追问:怎样防止调用点漏迁? 静态搜索、编译告警、弃用检查和分阶段 CI 统计。
  • 追问:何时可以移除旧 API? 调用方迁完且版本兼容策略允许时,通常在明确的大版本窗口。

十一、加强记忆

把大构造器或 JavaBean 重构为 Builder,不是简单换写法,而是把“字段含义、默认值、校验规则、对象不变量”重新收拢。先兼容、再迁移、最后收口,这样改动才稳。