建造者模式完全指南:从构造函数地狱到流式 API 的工程演化

"参数太多了"、"可选字段一堆"、"对象构造逻辑复杂"—— 这些都是建造者模式登场的信号。Lombok 的 @Builder、Java 的 StringBuilder、SQL 查询构造器、HTTP 客户端的链式 API,全都是建造者模式的产物。这篇文章把建造者从最朴素的需求讲到流式 API 的工程实现,讲清楚它和工厂、模板方法、链式调用的边界。

问题:构造函数地狱

看一个真实场景。订单对象有很多字段,有些必填,有些可选:

public class Order {
    private final String orderId;            // 必填
    private final String userId;             // 必填
    private final BigDecimal total;          // 必填
    private final String coupon;             // 可选
    private final String address;            // 可选
    private final String note;               // 可选
    private final Date scheduledTime;        // 可选
    private final boolean gift;              // 可选
    // ... 还有 5 个字段
}

朴素做法 1:给每种参数组合提供一个构造器。问题:必填 + 可选的组合是指数级的,提供完意味着十几个构造器,且每个看着差不多但参数顺序容易记错。

public Order(String id, String user, BigDecimal total) { ... }
public Order(String id, String user, BigDecimal total, String coupon) { ... }
public Order(String id, String user, BigDecimal total, String coupon, String address) { ... }
public Order(String id, String user, BigDecimal total, String address, String note) { ... }  // 参数顺序变了
// 用户调用时:new Order("o1", "u1", new BigDecimal("100"), null, "addr1", "note1") —— 哪个 null 是哪个?

朴素做法 2:setter 注入。问题:对象处于"中间态"时不完整;无法做成 immutable;并发场景下别的线程可能拿到半成品。

Order o = new Order();
o.setOrderId("o1");
o.setUserId("u1");
o.setTotal(new BigDecimal("100"));
// 这里如果有别的线程访问 o,会看到一个 total 已设但 coupon 还没设的不完整对象

建造者模式给了第三条路:把"逐步设置参数"和"一次性构造对象"分开

经典建造者结构

public class Order {
    // 所有字段 final,对象一旦构造完就不可变
    private final String orderId;
    private final String userId;
    private final BigDecimal total;
    private final String coupon;
    private final String address;
    private final String note;

    // 构造器私有 —— 外部只能通过 Builder 创建
    private Order(Builder b) {
        this.orderId = b.orderId;
        this.userId  = b.userId;
        this.total   = b.total;
        this.coupon  = b.coupon;
        this.address = b.address;
        this.note    = b.note;
    }

    public static Builder builder(String orderId, String userId, BigDecimal total) {
        return new Builder(orderId, userId, total);
    }

    public static class Builder {
        private final String orderId;          // 必填,Builder 构造时就要
        private final String userId;
        private final BigDecimal total;
        private String coupon;
        private String address;
        private String note;

        private Builder(String orderId, String userId, BigDecimal total) {
            this.orderId = orderId;
            this.userId = userId;
            this.total = total;
        }

        public Builder coupon(String c)  { this.coupon = c; return this; }
        public Builder address(String a) { this.address = a; return this; }
        public Builder note(String n)    { this.note = n; return this; }

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

        private void validate() {
            if (total.compareTo(BigDecimal.ZERO) <= 0)
                throw new IllegalArgumentException("total > 0");
            // 其他业务校验...
        }
    }
}

// 调用方
Order o = Order.builder("o1", "u1", new BigDecimal("100"))
    .coupon("SAVE10")
    .address("北京")
    .note("放门口")
    .build();

这种 API 几个特点立刻显现:

  • 必填和可选清楚分离:必填的进 builder 的构造函数,可选的链式调用。
  • 调用可读:.coupon("SAVE10")new Order("...", "...", ..., "SAVE10", ...) 自解释。
  • 对象不可变:Order 所有字段 final,构造完不能改 —— 天然线程安全。
  • 集中校验:在 build() 里统一校验所有规则,而不是散落在 setter 里。

Lombok 的 @Builder

上面的 Builder 写起来确实啰嗦。Lombok 一行注解搞定:

import lombok.Builder;
import lombok.Value;

@Value          // 自动加 final + getter + 不可变保证
@Builder        // 自动生成 Builder
public class Order {
    String orderId;
    String userId;
    BigDecimal total;
    String coupon;
    String address;
    String note;
}

// 调用完全一样
Order o = Order.builder()
    .orderId("o1").userId("u1").total(new BigDecimal("100"))
    .coupon("SAVE10").address("北京").build();

Lombok 的 @Builder 默认不区分必填/可选 —— 你需要用 @NonNull 标记或者在业务层校验。配合 Bean Validation 注解(@NotNull@DecimalMin 等)能把校验也搬到声明里。

带 director 的经典 GoF 建造者

GoF 原版建造者还有一个"指挥者(Director)"角色,管理构造步骤的顺序:

// 抽象建造者
interface HouseBuilder {
    void buildWalls();
    void buildRoof();
    void buildDoors();
    void buildWindows();
    House getResult();
}

// 具体建造者:不同房屋风格
class WoodHouseBuilder implements HouseBuilder { ... }
class StoneHouseBuilder implements HouseBuilder { ... }
class GlassHouseBuilder implements HouseBuilder { ... }

// 指挥者:定义构造顺序,与具体类型无关
class HouseDirector {
    public House construct(HouseBuilder builder) {
        builder.buildWalls();
        builder.buildRoof();
        builder.buildDoors();
        builder.buildWindows();
        return builder.getResult();
    }
}

// 使用
House wood = director.construct(new WoodHouseBuilder());
House stone = director.construct(new StoneHouseBuilder());

这种结构在游戏开发里有用 —— 一个"游戏对象建造流程"被指挥者固化,具体材质/属性由不同的 Builder 提供。日常 Java 业务里 Director 角色经常省略,直接用流式 API 表达。

实战 1:HTTP 客户端的请求构造

Java 的 HttpClient、OkHttp、Retrofit 全用建造者构造请求:

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/users"))
    .header("Accept", "application/json")
    .header("Authorization", "Bearer " + token)
    .timeout(Duration.ofSeconds(10))
    .POST(HttpRequest.BodyPublishers.ofString(json))
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

注意 HttpRequest 本身是不可变的 —— 一旦 build,所有字段都定型。这正是建造者最常见的用法:建造可变的 Builder,产出不可变的产品

实战 2:SQL 查询构造器

// jOOQ 风格的类型安全 SQL builder
DSLContext db = DSL.using(connection, SQLDialect.POSTGRES);

Result<Record3<Long, String, BigDecimal>> r = db.select(USERS.ID, USERS.NAME, USERS.BALANCE)
    .from(USERS)
    .where(USERS.STATUS.eq("active"))
    .and(USERS.BALANCE.gt(BigDecimal.valueOf(100)))
    .orderBy(USERS.CREATED_AT.desc())
    .limit(20)
    .fetch();

// MyBatis Plus 风格
QueryWrapper<User> q = new QueryWrapper<User>()
    .eq("status", "active")
    .gt("balance", 100)
    .orderByDesc("created_at")
    .last("limit 20");
List<User> users = userMapper.selectList(q);

SQL builder 实际上是"建造者 + 内部 DSL"的组合 —— 链式 API 不仅在搭建一个对象,本身就读起来像 SQL。这种设计让代码可读性接近 SQL 本身,又保留了类型安全。

实战 3:Fluent Test 框架

// AssertJ:断言 API 用建造者风格
assertThat(users)
    .isNotEmpty()
    .hasSize(3)
    .extracting(User::getName)
    .containsExactly("Alice", "Bob", "Charlie");

// 这种 API 本质是一个不断收窄的 Builder,每次链式返回一个不同的"上下文 Builder",
// 类型系统帮你保证下一步只能调和上下文匹配的方法

各语言里的建造者

Kotlin:apply / DSL

data class Order(
    val orderId: String,
    val userId: String,
    val total: BigDecimal,
    val coupon: String? = null,
    val address: String? = null,
    val note: String? = null,
)

// 用 apply 模拟 Builder 风格
val o = Order(orderId = "o1", userId = "u1", total = BigDecimal("100")).copy(
    coupon = "SAVE10", address = "北京", note = "门口",
)

// 或者写一个 DSL 函数
class OrderBuilder {
    var orderId: String = ""; var userId: String = ""; var total: BigDecimal = BigDecimal.ZERO
    var coupon: String? = null; var address: String? = null; var note: String? = null
    fun build() = Order(orderId, userId, total, coupon, address, note)
}
fun order(init: OrderBuilder.() -> Unit) = OrderBuilder().apply(init).build()

val o = order {
    orderId = "o1"; userId = "u1"; total = BigDecimal("100")
    coupon = "SAVE10"; address = "北京"
}

Python:dataclass + 命名参数

from dataclasses import dataclass
from decimal import Decimal
from typing import Optional

@dataclass(frozen=True)        # 不可变
class Order:
    order_id: str
    user_id: str
    total: Decimal
    coupon: Optional[str] = None
    address: Optional[str] = None
    note: Optional[str] = None

# Python 命名参数自带"建造者"效果
o = Order(
    order_id='o1', user_id='u1', total=Decimal('100'),
    coupon='SAVE10', address='北京',
)

Python / Kotlin 因为支持命名参数 + 默认值,大部分"建造者"场景已经被语言原生解决,不需要单独的 Builder 类。这也说明:建造者模式很大程度上是在弥补 Java 等语言"没有命名参数"的不足

TypeScript:对象字面量 + 类型

interface OrderOptions {
    orderId: string;
    userId: string;
    total: number;
    coupon?: string;
    address?: string;
    note?: string;
}

function createOrder(opts: OrderOptions): Order { ... }

createOrder({
    orderId: 'o1', userId: 'u1', total: 100,
    coupon: 'SAVE10', address: '北京',
});

渐进式 Builder:用类型保证调用顺序

普通 Builder 允许任意顺序调用,但build() 时才发现"必填字段没设"。能不能让"没调用 .orderId() 就调 .build()"成为编译错误?可以,用"渐进式 Builder"(step builder):

// 每个状态一个接口,只暴露下一步能调的方法
interface NeedOrderId { NeedUserId orderId(String id); }
interface NeedUserId  { NeedTotal userId(String id); }
interface NeedTotal   { OptionalSteps total(BigDecimal t); }
interface OptionalSteps {
    OptionalSteps coupon(String c);
    OptionalSteps address(String a);
    Order build();
}

class OrderBuilder implements NeedOrderId, NeedUserId, NeedTotal, OptionalSteps {
    private String orderId, userId, coupon, address;
    private BigDecimal total;
    public NeedUserId orderId(String id) { this.orderId = id; return this; }
    public NeedTotal  userId(String id)  { this.userId = id; return this; }
    public OptionalSteps total(BigDecimal t) { this.total = t; return this; }
    public OptionalSteps coupon(String c) { this.coupon = c; return this; }
    public OptionalSteps address(String a) { this.address = a; return this; }
    public Order build() { return new Order(orderId, userId, total, coupon, address); }
}

public class Order {
    public static NeedOrderId builder() { return new OrderBuilder(); }
}

// 调用:IDE 会强制你按顺序调
Order.builder().orderId("o1").userId("u1").total(BigDecimal.TEN)
    .coupon("X").build();
// Order.builder().userId(...)  -> 编译错误:NeedOrderId 没这个方法

渐进式 Builder 写起来麻烦,但 API 用户体验极好 —— 错误从运行期前置到编译期。值不值得,看产品调用频次。

建造者 vs 工厂 vs 模板方法

  • 工厂:关注"创建什么对象",一次性返回。
  • 建造者:关注"如何分步组装一个复杂对象",可逐步参数化。
  • 模板方法:关注"流程固定,某些步骤可替换",一个父类多个子类。

三者经常组合:工厂方法返回一个 Builder,Builder 内部用模板方法处理可定制步骤。Spring 的 RestTemplate.BuilderWebClient.Builder 就是这种组合的实例。

常见坑

坑 1:Builder 字段写漏。 给 Order 加一个新字段,忘了在 Builder 加 setter / 在 build() 传过去 —— 这是手写 Builder 最常见的 bug。用 Lombok / 自动生成能规避。

坑 2:Builder 共享。 把一个 builder 实例缓存重用是危险的 —— 上一次 build 后字段还在里面,下次 build 出来的对象会继承上次的部分字段。规范做法是"用完即弃":每次构造都新建 builder。

坑 3:可变 Builder + 不可变 Product 的混淆。 调用方拿到的应该是不可变 Product,而不是 Builder 自己。某些代码错把 Builder 实例本身当成"配置对象"传来传去,失去不可变性。

坑 4:过度建造者化。 三五个参数的简单对象不需要 Builder,直接构造器就够。Builder 的成本是"多一个类、多一组样板代码",收益是"参数 ≥ 4 时可读性提升"。前者不抵后者就不要上。

写在最后

建造者模式不是炫技,它解决的是一个朴素的工程问题:当一个对象的构造参数多到让 API 难以使用时,把"参数收集"和"对象构造"分两步走。在 Java / C++ 这种缺少命名参数的语言里,Builder 几乎是必备;在 Kotlin / Python / TS 里,语言本身能解决一半需求,Builder 留给那些真正需要校验或步骤化的场景。

给一个决定何时用 Builder 的简单标准:当构造器超过 4 个参数,或者有 3 个以上可选参数,或者构造时需要复杂校验/计算,就考虑 Builder。不到这个量级,直接构造器或对象字面量更轻量。设计模式不是给每个场景都套上,它是当你的代码自己"喊出"需求时,才被引入。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

抽象工厂模式完全指南:让一族产品保持配套的设计利器

2026-5-15 11:35:34

技术教程

原型模式完全指南:从浅克隆到深拷贝再到不可变共享

2026-5-15 11:50:57

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索