"参数太多了"、"可选字段一堆"、"对象构造逻辑复杂"—— 这些都是建造者模式登场的信号。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.Builder、WebClient.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