建造者模式:复杂对象的构建、链式调用、实战:Lombok @Builder 原理
说实话,刚入行那会儿,我写过一个订单类,构造函数有十几个参数。每次 new 对象,都得对着参数列表数半天,生怕传错顺序。后来重构时用了建造者模式,代码瞬间清爽了。今天我们就聊聊这个模式,以及它背后的链式调用和 Lombok 的 @Builder 原理。
为什么需要建造者模式?
你想想看,如果一个对象有 5 个必填字段、3 个可选字段,你怎么办?
- 用重叠构造器?参数多了自己都晕。
- 用 setter?对象可能处于不一致状态。
- 用 JavaBean 模式?线程不安全。
建造者模式就是来解决这个问题的。它把对象的构建过程和表示分离开,让你可以一步步构造复杂对象,最后一步才生成成品。
核心思想:用 Builder 对象封装构造逻辑,客户端只需要告诉 Builder「我要什么」,不用关心「怎么造」。
经典建造者模式结构
我习惯把建造者模式拆成四个角色:
| 角色 | 职责 | 例子 |
|---|---|---|
| Product(产品) | 最终要构建的复杂对象 | Order 订单类 |
| Builder(抽象建造者) | 定义构建步骤的接口 | OrderBuilder 接口 |
| ConcreteBuilder(具体建造者) | 实现构建步骤,维护产品状态 | OrderBuilderImpl |
| Director(指挥者) | 控制构建顺序(可选) | OrderDirector |
不过在实际项目中,我很少单独写 Director 类。大部分场景下,客户端直接调用 Builder 的方法就够了。Director 更适合那些构建步骤固定、且需要复用的场景。
链式调用的实现
链式调用说白了就是每个 setter 方法都返回 this。但这里有个关键点:返回的是 Builder 本身,而不是 Product。
public class Order {
private String orderId;
private String userId;
private List<String> items;
private String couponCode;
private String remark;
private Order() {}
public static class Builder {
private String orderId;
private String userId;
private List<String> items = new ArrayList<>();
private String couponCode;
private String remark;
public Builder orderId(String orderId) {
this.orderId = orderId;
return this;
}
public Builder userId(String userId) {
this.userId = userId;
return this;
}
public Builder addItem(String item) {
this.items.add(item);
return this;
}
public Builder couponCode(String couponCode) {
this.couponCode = couponCode;
return this;
}
public Builder remark(String remark) {
this.remark = remark;
return this;
}
public Order build() {
// 这里可以做校验
if (orderId == null || userId == null) {
throw new IllegalStateException("orderId 和 userId 不能为空");
}
Order order = new Order();
order.orderId = this.orderId;
order.userId = this.userId;
order.items = this.items;
order.couponCode = this.couponCode;
order.remark = this.remark;
return order;
}
}
}
使用的时候就很优雅了:
Order order = new Order.Builder()
.orderId("ORD2024001")
.userId("U10086")
.addItem("iPhone 15")
.addItem("AirPods Pro")
.couponCode("VIP50")
.remark("请用顺丰")
.build();
我的习惯:在 build() 方法里做参数校验。这样客户端拿到的 Order 对象一定是合法的,不会出现半成品。
Lombok @Builder 原理
说实话,自从用了 Lombok 的 @Builder,我手写 Builder 的次数少了很多。但理解它的原理很重要,不然出了问题你都不知道怎么排查。
@Builder 注解在编译期会生成一个静态内部 Builder 类,原理和我们手写的几乎一样。但有几个细节要注意:
- 全字段构造:@Builder 默认会为所有字段生成 setter 方法。
- build() 方法:会调用一个全参构造器(Lombok 自动生成的)。
- toBuilder 模式:加上
@Builder(toBuilder = true),可以从已有对象创建一个 Builder,方便修改部分字段。
@Builder
@ToString
public class User {
private String name;
private int age;
private String email;
}
// 使用
User user = User.builder()
.name("张三")
.age(28)
.email("zhangsan@example.com")
.build();
// toBuilder 模式
User updatedUser = user.toBuilder()
.age(29)
.build();
我曾经踩过的坑:@Builder 和 @NoArgsConstructor 一起用会报错。因为 @Builder 需要全参构造器,而 @NoArgsConstructor 会生成无参构造器,两者冲突。解决方案是加上 @AllArgsConstructor,或者用 @Builder 的 @Builder.Default 给字段设默认值。
建造者模式 vs 工厂模式
很多初学者会混淆这两个模式。我简单说下区别:
| 维度 | 建造者模式 | 工厂模式 |
|---|---|---|
| 关注点 | 对象的构建过程(步骤多) | 对象的创建(一步到位) |
| 复杂度 | 适合复杂对象 | 适合简单对象 |
| 灵活性 | 客户端可控制构建步骤 | 客户端不关心构建细节 |
| 典型场景 | 订单、查询条件、配置对象 | 数据库连接、日志记录器 |
说白了,工厂模式是「给我一个对象」,建造者模式是「我要这样这样那样那样,最后给我一个对象」。
实战:构建一个复杂的查询条件
我在项目中遇到过这样一个场景:用户查询订单时,有十几个筛选条件,包括时间范围、金额范围、状态、支付方式、排序方式等等。如果用传统方式,要么写一个超长的方法签名,要么用 Map 传参——都不优雅。
用建造者模式就舒服多了:
public class OrderQuery {
private String orderId;
private Date startTime;
private Date endTime;
private BigDecimal minAmount;
private BigDecimal maxAmount;
private List<String> statusList;
private String payMethod;
private String sortField;
private boolean asc;
private OrderQuery() {}
public static class Builder {
// 字段省略...
public Builder orderId(String orderId) { ... return this; }
public Builder timeRange(Date start, Date end) { ... return this; }
public Builder amountRange(BigDecimal min, BigDecimal max) { ... return this; }
public Builder status(String... statuses) { ... return this; }
public Builder payMethod(String payMethod) { ... return this; }
public Builder sort(String field, boolean asc) { ... return this; }
public OrderQuery build() {
// 校验逻辑
if (startTime != null && endTime != null && startTime.after(endTime)) {
throw new IllegalArgumentException("开始时间不能晚于结束时间");
}
// 构建对象
OrderQuery query = new OrderQuery();
// 赋值...
return query;
}
}
}
// 使用
OrderQuery query = new OrderQuery.Builder()
.timeRange(beginOfDay, endOfDay)
.status("PAID", "SHIPPED")
.sort("createTime", false)
.build();
你看,这样调用方一眼就能看出查询条件是什么,可读性极好。
建造者模式的核心流程图
什么时候用建造者模式?
我总结了几条经验:
- 对象字段超过 4 个,且部分可选。这时候构造函数已经不太够用了。
- 对象创建后不希望被修改。建造者模式配合不可变对象,非常安全。
- 构建过程有顺序依赖。比如必须先设置 A 才能设置 B,可以在 Builder 里做状态控制。
- 需要生成多种表现形式。同一个 Builder 接口,不同实现可以产出不同产品。
我的建议:不要为了用模式而用模式。如果对象只有两三个字段,直接 new 或者用静态工厂方法就够了。过度设计也是种坏味道。
避坑指南
最后分享几个我踩过的坑:
- 线程安全:Builder 本身不是线程安全的。如果多个线程共享同一个 Builder 实例,记得加锁或者每个线程 new 一个。
- 继承问题:如果产品类有继承关系,Builder 也要跟着继承。Lombok 的 @Builder 对继承支持不太好,需要手动处理。
- 性能损耗:每次构建都会创建 Builder 对象,如果构建频率极高(比如每秒百万次),要考虑对象创建的开销。
- 不要滥用链式调用:链式调用虽然好看,但如果方法调用链太长(超过 10 个),反而影响可读性。
嗯,建造者模式就聊到这儿。记住一句话:当构造函数的参数列表让你想骂娘的时候,就该用建造者模式了。