告别烦人的 setter
和超长构造函数,Builder 模式真香!
咱们先不谈 Builder
,回想一下,如果没有它,我们要创建一个复杂的对象(就像你贴的那个 UserInteraction
),通常有几种方法?
噩梦之一:望远镜一样的构造函数 (Telescoping Constructor)
最直接的想法,就是用构造函数。但问题来了,UserInteraction
这个对象有很多字段,其中一些是必需的(比如 userId
, itemId
),另一些是可选的(比如 rating
, sessionId
)。
那构造函数该怎么写?
// 为了方便,我们先叫它 Interaction
public class Interaction {
private Long userId; // 必填
private Long itemId; // 必填
private String interactionType; // 必填
private Double rating; // 可选
private Instant timestamp; // 可选,有默认值
private String sessionId; // 可选
// ...还有一堆其他字段
// 只有必填项的构造函数
public Interaction(Long userId, Long itemId, String interactionType) {
this(userId, itemId, interactionType, null, null);
}
// 必填项 + rating 的构造函数
public Interaction(Long userId, Long itemId, String interactionType, Double rating) {
this(userId, itemId, interactionType, rating, null);
}
// 必填项 + rating + sessionId 的构造函数
public Interaction(Long userId, Long itemId, String interactionType, Double rating, String sessionId) {
// ...
this.userId = userId;
this.itemId = itemId;
// ...
}
// 后面还有 N 个构造函数... 简直是灾难!
}
这种写法叫“伸缩构造函数模式” (Telescoping Constructor Pattern)。当参数少的时候还行,一旦超过4、5个,就会变得极难维护。而且,在调用的时候,代码可读性极差:
// 这行代码谁看得懂 123L 和 456L 分别是啥?很容易传反了
Interaction interaction = new Interaction(123L, 456L, "click", 5.0, "session-xyz");
噩梦之二:随时可能“裸奔”的 JavaBean 模式
“好吧,构造函数这么麻烦,我用空构造函数 + setter 总行了吧?” 这就是 JavaBean 模式,也是我一开始最喜欢用的。
public class Interaction {
private Long userId;
private Long itemId;
// ... 其他字段
// 空构造函数
public Interaction() {}
// 一堆 getter 和 setter
public void setUserId(Long userId) { this.userId = userId; }
public Long getUserId() { return this.userId; }
// ...
}
调用起来好像还不错,可读性也好了:
Interaction interaction = new Interaction();
interaction.setUserId(123L);
interaction.setItemId(456L);
interaction.setInteractionType("click");
interaction.setRating(5.0);
但是,这种方式有几个致命的缺点:
- 对象状态不一致:在调用完所有
setter
之前,这个interaction
对象其实是一个“半成品”。它的userId
可能是null
,itemId
也可能是null
。如果你把这个半成品传来传去,天知道哪个环节会因为它缺少必要信息而抛出NullPointerException
。 - 对象是可变的 (Mutable):
setter
方法让这个对象在创建之后,随时都可以被修改。这在多线程环境下尤其危险,你无法保证一个对象的状态不被其他线程意外篡改。一个好的数据对象(DTO),我们通常希望它是不可变的 (Immutable),一旦创建,就跟“刻在石头上”一样,内容不能再变,这样才安全。
Builder 模式如何拯救世界?
好了,铺垫了这么多,主角 Builder
终于要登场了。Builder
模式就是为了解决上面这两种噩梦而生的。
我们再来看你给的这段代码,它的完整形态应该是这样的:
// 这是我们最终想得到的、不可变的 Interaction 对象
public final class Interaction {
private final Long userId; // final 关键字,保证不可变
private final Long itemId;
private final String interactionType;
private final Double rating;
private final Instant timestamp;
// ...
// 构造函数是 private 的!只能被内部的 Builder 调用
private Interaction(Builder builder) {
this.userId = builder.userId;
this.itemId = builder.itemId;
this.interactionType = builder.interactionType;
this.rating = builder.rating;
this.timestamp = builder.timestamp;
// ...
}
// 只有 getter,没有 setter
// ---- 这就是 Builder,一个静态内部类 ----
public static class Builder {
// Builder 内部有和外部类一模一样的字段
private Long userId; // 必填
private Long itemId; // 必填
private String interactionType; // 必填
private Double rating;
private Instant timestamp = Instant.now();
// ...
// 构造函数只接收必填项,保证基础数据完整
public Builder(Long userId, Long itemId, String interactionType) {
this.userId = userId;
this.itemId = itemId;
this.interactionType = interactionType;
}
// 每个 setter 方法都返回 Builder 本身 (return this)
public Builder rating(Double rating) {
this.rating = rating;
return this;
}
public Builder timestamp(Instant timestamp) {
this.timestamp = timestamp;
return this;
}
// ... 其他可选参数的 "setter"
// 最后,用一个 build() 方法来创建真正的 Interaction 对象
public Interaction build() {
// 在这里可以加一些校验逻辑
return new Interaction(this);
}
}
}
Builder 的底层原理和核心思想
Builder
模式的原理其实很简单,它就像一个“订单生成器”:
-
分离构建与表示:它将一个复杂对象的构建过程(由
Builder
类负责)和它的最终表示(Interaction
类本身)分离开来。Builder
是一个临时的、可变的助手,而Interaction
是最终的、不可变的成果。 -
链式调用 (Fluent Interface):
Builder
里的rating(...)
、timestamp(...)
这些方法,在设置完内部值后,都会return this;
,也就是返回Builder
对象自己。这使得我们可以像链条一样把方法调用串起来,形成了非常流畅的代码:Interaction interaction = new Interaction.Builder(123L, 456L, "click") .rating(5.0) .timestamp(Instant.now()) .build();
-
最终一锤定音:
build()
方法是整个过程的最后一步。它会调用Interaction
的私有构造函数,并将Builder
自己(this
)传进去。Interaction
的构造函数会从Builder
对象中“拷贝”所有数据到自己的final
字段里。一旦build()
完成,一个完整的、不可变的Interaction
对象就诞生了,而那个临时的Builder
助手就可以功成身退了。
我们为什么需要 Builder?
总结一下,我现在爱上它的理由:
- 极高的可读性:
.userId(123L)
这种写法,参数的意义一目了然,代码即是文档,再也不怕把参数顺序搞错了。 - 优雅处理可选参数:对于可选参数,比如
rating
,我需要就调用.rating()
,不需要就不调用,非常灵活,彻底告别了那一长串的构造函数。 - 保证对象的不变性 (Immutability):这是最重要的一点!
Interaction
对象没有setter
,字段都是final
的,一旦通过build()
创建出来,就是线程安全的,可以放心地在系统里传递,再也不用担心它的状态被意外修改。 - 保证对象状态的完整性:我们可以在
Builder
的构造函数里强制传入所有必填项,在build()
方法里还可以增加更复杂的校验逻辑。这确保了我们永远不会得到一个“半成品”对象,只要对象被创建出来,它的状态就是有效和完整的。
虽然写 Builder
模式在初期会增加一些代码量,但它换来的是代码的可维护性、可读性和健壮性的大幅提升。对于那些拥有超过3、4个参数的复杂对象,尤其是数据传输对象(DTO),这种投入绝对是物超所值的。
自从我领会了 Builder
模式的妙处,基本上项目中所有复杂的数据对象都用它来构建了。不知道大家在项目中还有没有发现其他特别好用的设计模式?欢迎留言分享一下!