Skip to content
Go back

建造者模式——复杂对象构建的优雅之道

建造者模式:复杂对象构建的优雅之道

一句话结论(30s)

建造者模式的本质是把对象构建过程与对象本身分离,因为多参构造器存在位置依赖、相邻同类型参数极易传反、可选参数只能用 null 占位。关键设计是「命名方法替代位置参数 + setter 返回 this 链式调用 + build() 校验」,代价是代码量增加,所以超过 5 个参数(阿里规范)才值得用。

核心原理(2min)

主流程:Builder 构造器强制传入必填字段 → 可选字段通过 setter 设置并返回 this → build() 校验后返回成品。关键设计三点:Builder 构造器参数 = 必填字段(编译期校验、绝不遗漏)、每个 setter 返回 this(Fluent API 链式调用)、build() 集中校验并返回不可变对象。源码落地:StringBuilder 是简化版 Builder(append 返回 this、toString 相当于 build),OkHttp 的 Request.Builder 构建全 final 的不可变 Request,MyBatis 的 SqlSessionFactoryBuilder 是「用完即弃」的一次性建造者。

底层深入(5-10min)

从一段”丑陋”的构造器开始

public Order(String orderId, String userId, BigDecimal amount,
             String couponId, String address, String remark,
             boolean insured, int deliveryType, String giftCardId) {
    // 9 个参数,调用时谁记得住顺序?
}

// 调用端:这是什么鬼?
Order order = new Order("O001", "U123", new BigDecimal("99.00"),
    null, "北京市朝阳区", null, true, 1, null);

参数一多,构造函数就成了灾难:可读性差(第 4 个 null 是什么?)、容易传错位置(把 address 传到了 couponId)、可选参数用 null 占位。建造者模式(Builder Pattern)正是为解决这个问题而生。

思考穿插:为什么「相邻同类型参数」特别危险? 因为编译器只管类型、不管语义——两个 String 挨在一起,把地址传进 couponId 的位置,编译期不会报错,运行时才炸。位置参数的本质是「用顺序编码语义」,顺序一错语义就错;Builder 的命名方法(.address(x).couponId(y))是把语义显式写出来,从根源上消灭了「顺序依赖」这一类 bug。

建造者模式四要素

建造者模式将对象的构建过程对象本身分离:

Product(产品)  ←  Director(指挥者,可选)
                      ↓ 调用
                   Builder(抽象建造者)
                      ↑ 实现
                ConcreteBuilder(具体建造者)

核心思想:一步一步构建复杂对象,最后调用 build() 返回成品。

标准代码模板

public class Order {
    private String orderId;
    private String userId;
    private BigDecimal amount;

    private Order(Builder builder) {
        this.orderId = builder.orderId;
        this.userId = builder.userId;
        this.amount = builder.amount;
    }

    // 静态内部类 Builder
    public static class Builder {
        private String orderId;    // 必填
        private String userId;     // 必填
        private BigDecimal amount; // 必填

        // 可选参数 → setter 返回 this(链式调用)
        private String couponId;
        private String address;
        private String remark;

        public Builder(String orderId, String userId, BigDecimal amount) {
            this.orderId = orderId;
            this.userId = userId;
            this.amount = amount;
        }

        public Builder couponId(String couponId) {
            this.couponId = couponId;
            return this;  // ← 链式调用的关键
        }

        public Builder address(String address) {
            this.address = address;
            return this;
        }

        public Builder remark(String remark) {
            this.remark = remark;
            return this;
        }

        public Order build() {
            return new Order(this);
        }
    }
}

// 调用端:一目了然
Order order = new Order.Builder("O001", "U123", new BigDecimal("99.00"))
    .address("北京市朝阳区")
    .remark("工作日配送")
    .build();

关键设计决策

1. Builder 的构造器参数 = 必填字段。 必填字段通过构造器强制传入(编译期校验),可选字段通过 setter 设置。这样调用端绝不会遗漏必填字段。

2. 每个 setter 返回 this 这是链式调用(Fluent API)的核心——每个方法返回 Builder 自身,调用端可以连续 .xxx().yyy().zzz()

3. build() 中可以加校验逻辑。 比如金额不能为负、orderId 必须匹配格式等。校验放在 build() 中,比散落在各处安全得多。

思考穿插:Builder 为什么要用静态内部类,而不是单独的类? 一是静态内部类可以直接访问外部类的私有字段和私有构造器(new Order(this) 能调 private 构造器),省掉一堆 getter/setter;二是把 Builder 和产品绑定在一起,语义上「这个 Builder 就是造 Order 的」,命名也不污染包空间。同时产品构造器设为 private、字段 final,就能保证「对象一旦 build 出来就是不可变的、只能由 Builder 造」。

经典源码解析

StringBuilder:简化版 Builder

StringBuilder 本质上是一个”字符串建造者”。我们不断 append() 追加字符,最后 toString() 返回结果:

StringBuilder sb = new StringBuilder();
sb.append("SELECT * FROM ").append(tableName).append(" WHERE id = ").append(id);
String sql = sb.toString();

JDK 真实源码里,append() 之所以能链式调用,是因为每个重载都 return this

    @Override
    public StringBuilder append(Object obj) {
        return append(String.valueOf(obj));
    }

    @Override
    @IntrinsicCandidate
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }

toString() 扮演的就是 build() 的角色,收尾时把内部缓冲字符数组真正转成一个新的 String 返回:

    @Override
    @IntrinsicCandidate
    public String toString() {
        if (length() == 0) {
            return "";
        }
        // Create a copy, don't share the array
        return new String(this, null);
    }

append(Object) 先把任意对象转成字符串再委托给 append(String)append(String) 调用父类的追加逻辑后 return this——正是 Builder 模式里「setter 返回 this 实现链式调用」的标准写法。toString() 相当于 build(),它在收尾时才 new String(...) 复制一份,避免把内部可变缓冲区直接暴露出去,这也呼应了 Builder 模式「最终返回不可变成品」的设计。

append() 返回 this(链式调用),toString()build()。StringBuilder 省去了 Director 角色——调用端自己就是指挥者。

OkHttp Request.Builder

OkHttp 的 Request 有几十个可配置项(URL、method、headers、body、timeout 等),构造器根本无法覆盖所有组合:

Request request = new Request.Builder()
    .url("https://api.example.com/user")
    .header("Authorization", "Bearer xxx")
    .header("Content-Type", "application/json")
    .post(RequestBody.create(json, JSON))
    .build();

OkHttp 的 Builder 还做了**不可变对象(Immutable Object)**优化:build() 返回的 Request 是不可变的(所有字段都 final),Builder 是可变的中间状态。构建完毕后,Request 的线程安全性由不可变性天然保证。

MyBatis SqlSessionFactoryBuilder

MyBatis 的 SqlSessionFactoryBuilder 是”用完即弃”的建造者:

SqlSessionFactory factory = new SqlSessionFactoryBuilder()
    .build(Resources.getResourceAsStream("mybatis-config.xml"));

build() 执行后,Builder 内部的 XML 解析结果全部转移到 SqlSessionFactory,Builder 本身不再有用。这种模式适合”一次性构建”场景——工厂对象创建成本高、创建后不需要重建。

阿里规范:超过 5 个属性用 Builder

《阿里巴巴 Java 开发手册》明确规定:构造方法的参数超过 5 个时,建议使用 Builder 模式。

原因不仅仅是可读性。多参数构造器存在参数顺序依赖——相同类型的相邻参数极容易传反:

// 两个 String 相邻,传反了编译器不会报错
new User("张三", "北京市", "13800138000", "北京市", "工程师");
//               ↑ name          ↑ phone? address? 谁分得清?

Builder 用命名方法替代位置参数,从根源上消除了这个隐患。而且 Builder 天然支持部分构建——你可以先构造到一半,根据条件决定是否继续添加可选字段,构造函数做不到。

Builder vs 其他创建模式

Builder工厂方法构造函数
参数个数多(>4)
参数可选天然支持不支持不支持(需重载)
构建步骤可控顺序一次性一次性
不可变对象天然支持需手动需手动
复杂度较高

思考穿插:Builder 和工厂方法怎么选? 看「参数多不多、要不要可选参数、要不要可控的构建步骤」。工厂方法适合「参数少、创建逻辑要统一封装」的场景,一个方法调完即得;Builder 适合「参数多(>4)、大量可选参数、且 build 前要逐步配置」的场景。一句话:工厂管「造什么」,Builder 管「怎么一步步造出来」。

总结

建造者模式解决了”参数爆炸”问题。核心技巧只有三个:Builder 内部类持有所有字段、setter 返回 this 实现链式调用、build() 中校验并返回不可变对象。 超过 5 个参数就果断用 Builder——这是从无数线上 bug 中总结出的工程实践,不是教条。

章末提问

Q1:Builder 和工厂方法模式的区别? 结论先行:工厂管「造什么」、Builder 管「怎么一步步造出来」。因为工厂适合参数少、创建逻辑要统一封装的场景,一次调用即得;Builder 适合参数多、大量可选参数、且需要逐步配置再 build 的场景,把构建过程与对象本身分离。

Q2:为什么 setter 要返回 this? 结论先行:为了实现 Fluent API 的链式调用,让「配置过程」读起来像一句话。因为每个 setter 返回 Builder 自身,调用端才能连续 .address(x).remark(y).build();如果返回 void,就只能一行一个语句,失去 Builder 的可读性核心。

Q3:为什么 Builder 的构造器参数要等于必填字段? 结论先行:把「必填校验」从运行时提前到编译期。因为必填字段塞进 Builder 构造器后,调用端不传就没法编译通过,绝不会出现「忘传某个必填字段」的运行时 bug;可选字段才走 setter。

Q4:StringBuilder 是不是标准的建造者模式?少了什么? 结论先行:是简化版 Builder,但省掉了 Director(指挥者)角色。因为 append() 返回 this 是 setter 链式调用,toString() 是 build();但调用端自己直接指挥追加顺序,没有独立的 Director,所以是「去掉指挥者的简化版」。

Q5:为什么阿里规范建议超过 5 个参数用 Builder? 结论先行:因为多参构造器有位置依赖,相邻同类型参数极易传反且编译器不报错。Builder 用命名方法替代位置参数,从根源消除「顺序编码语义」的隐患,还天然支持可选参数与不可变对象——代价是代码量增加,所以只在参数多时才值得。


Share this post on:

Previous Post
模板方法模式——JdbcTemplate如何把连接管理从业务代码中抽离
Next Post
原型模式——对象克隆的深浅之道