8.2 Builder、Prototype 与实例作用域
工匠们为复杂对象写出了越来越长的构造函数,复制和全局实例又带来新的生命周期问题。
Factory 解决“选择哪种实现”,Builder 解决“怎样把一个复杂对象组装完整”,Prototype 解决“怎样从现有对象派生新对象”。Singleton 则不是组装算法,而是对实例作用域施加限制。
Builder:把可选配置与最终不变量分开
构造器参数很多并不自动需要 Builder。若参数都必填,一个参数对象或记录类型可能更诚实。Builder 更适合:
- 可选项较多;
- 创建过程分多步收集信息;
- 构建前需要跨字段校验;
- 希望最终对象保持不可变。
public final class TournamentPlan {
private final String name;
private final int capacity;
private final java.time.Duration matchDuration;
private final boolean ranked;
private final Integer minimumLevel;
private TournamentPlan(Builder b) {
this.name = b.name;
this.capacity = b.capacity;
this.matchDuration = b.matchDuration;
this.ranked = b.ranked;
this.minimumLevel = b.minimumLevel;
}
public static Builder builder(String name, int capacity) {
return new Builder(name, capacity);
}
public static final class Builder {
private final String name;
private final int capacity;
private java.time.Duration matchDuration =
java.time.Duration.ofMinutes(15);
private boolean ranked;
private Integer minimumLevel;
private Builder(String name, int capacity) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("name is blank");
}
if (capacity < 2) {
throw new IllegalArgumentException("capacity < 2");
}
this.name = name;
this.capacity = capacity;
}
public Builder matchDuration(java.time.Duration value) {
this.matchDuration = java.util.Objects.requireNonNull(value);
return this;
}
public Builder rankedFromLevel(int level) {
this.ranked = true;
this.minimumLevel = level;
return this;
}
public TournamentPlan build() {
if (matchDuration.isZero() || matchDuration.isNegative()) {
throw new IllegalStateException("match duration must be positive");
}
if (ranked && minimumLevel < 1) {
throw new IllegalStateException("minimum level < 1");
}
return new TournamentPlan(this);
}
}
}必填参数在 builder() 入口出现;可选参数有领域化名称;最终对象只在 build() 验证通过后产生。
Builder 自身通常可变且不保证线程安全,不应跨线程共享。构建完成后也不要让最终对象保留对 Builder 可变集合的引用;使用 List.copyOf 等方式取得快照。
Builder 不是 setter 的另一种拼法
以下 Builder 仍能制造非法对象:
builder.name(null).capacity(-1).build();如果所有字段都可任意设置,build() 也不校验,它只是把 setter 改成链式语法。真正价值在于:
- 区分必填与可选;
- 用方法名表达有效组合;
- 在一个位置维护跨字段不变量;
- 隐藏中间态,不让半成品进入业务系统。
当构建顺序本身必须受类型约束时,可以使用 staged builder,但接口数量和泛型复杂度会增加。多数业务对象用运行时校验已经足够。
Prototype:先定义复制语义
“复制”至少有三种可能:
- 浅复制:新外层对象,共享内部引用;
- 深复制:递归复制整张可达对象图;
- 领域复制:只复制被定义为模板的值,重置 ID、时间戳和运行状态。
业务系统通常需要第三种,而不是无差别深拷贝。
public record TournamentTemplate(
String name,
int capacity,
java.time.Duration matchDuration,
java.util.List<RewardRule> rewardRules) {
public TournamentTemplate {
rewardRules = java.util.List.copyOf(rewardRules);
}
public TournamentTemplate withName(String newName) {
return new TournamentTemplate(
newName, capacity, matchDuration, rewardRules);
}
public Tournament instantiate(TournamentId newId,
java.time.Instant createdAt) {
return Tournament.draft(
newId, name, capacity, matchDuration,
rewardRules, createdAt);
}
}不可变 RewardRule 可以安全共享,不必为了“深拷贝”制造重复对象;实体 ID 和创建时间则在实例化时重新生成。复制策略直接写在类型 API 中,比通用 clone() 更容易审查。
Java 的 Cloneable 只是一种标记,并不声明公开 clone() 方法;Object.clone() 默认执行字段级浅复制。对拥有可变引用、资源句柄或继承层次的对象,显式复制构造器、命名复制方法或不可变值通常更安全。
序列化再反序列化不是通用深拷贝方案:它把复制语义绑定到序列化格式,还会带来性能、版本、类型和安全问题。
Singleton:先问“在哪个范围唯一”
“系统只有一个实例”必须说明范围:
每次方法调用?每个请求?每个线程?
每个 DI 容器?每个 class loader?每个 JVM 进程?
整个集群?整个租户?Java 静态字段最多约束到加载该类的 class loader;多进程部署中每个进程仍有自己的实例。它不能实现集群级唯一,也不能替代分布式锁或数据库唯一约束。
全局访问与单实例是两件事
public final class RegistrationService {
private final Clock clock;
public RegistrationService(Clock clock) {
this.clock = clock;
}
}装配层可以只创建一个 Clock 或服务实例,再注入所有调用者;业务代码无需 GlobalClock.getInstance()。这样既控制实例数量,又保持依赖显式和可测试。
全局 Singleton 常见问题包括:
- 隐藏依赖和初始化顺序;
- 可变状态在测试间泄漏;
- 并发规则不清楚;
- 以后需要多租户、多配置或多个连接池时难拆;
- 生命周期与关闭顺序难管理。
日志 facade、枚举常量或无状态共享对象可能适合全局入口,但应把可变配置和资源生命周期放在明确边界。
Java 中线程安全初始化不等于线程安全对象
public enum ProcessIdentity {
INSTANCE;
private final java.util.UUID id = java.util.UUID.randomUUID();
public java.util.UUID id() { return id; }
}枚举可以提供 JVM/class-loader 范围内可靠的初始化与序列化语义,但如果 Singleton 内部持有可变 Map、计数器或连接,仍需单独设计同步、可见性和关闭行为。“单例创建是线程安全的”不能推出“所有方法都线程安全”。
五种模式的选择表
| 模式 | 主要变化点 | 典型风险 |
|---|---|---|
| Factory Method | 父类流程中的产品创建步骤 | 为简单选择引入不必要继承 |
| Abstract Factory | 整族产品的具体实现 | 给产品族增加新种类代价高 |
| Builder | 复杂对象的组装与校验 | 半成品泄露、只换皮的 setter |
| Prototype | 从现有值派生对象 | 浅/深/领域复制语义不清 |
| Singleton | 某一范围内的实例数量 | 全局状态、隐藏依赖、误判作用域 |
模式名不是目标。先写清楚变化点、生命周期、所有权和不变量,再选择最小的创建机制。
练习
选择一个真实对象,分别回答:
- 哪些参数必填,哪些可选?
- 直接构造为何不足?
- 如果使用 Builder,哪些非法组合必须在
build()拒绝? - 如果从模板复制,哪些字段共享、复制或重置?
- 如果只创建一个实例,这个“一”具体属于哪个范围?
如果这些问题答不清楚,先不要选择模式。