为什么Java更建议使用Builder模式编码?

告别烦人的 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);

但是,这种方式有几个致命的缺点:

  1. 对象状态不一致:在调用完所有 setter 之前,这个 interaction 对象其实是一个“半成品”。它的 userId 可能是 nullitemId 也可能是 null。如果你把这个半成品传来传去,天知道哪个环节会因为它缺少必要信息而抛出 NullPointerException
  2. 对象是可变的 (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 模式的原理其实很简单,它就像一个“订单生成器”:

  1. 分离构建与表示:它将一个复杂对象的构建过程(由 Builder 类负责)和它的最终表示Interaction 类本身)分离开来。Builder 是一个临时的、可变的助手,而 Interaction 是最终的、不可变的成果。

  2. 链式调用 (Fluent Interface)Builder 里的 rating(...)timestamp(...) 这些方法,在设置完内部值后,都会 return this;,也就是返回 Builder 对象自己。这使得我们可以像链条一样把方法调用串起来,形成了非常流畅的代码:

    Interaction interaction = new Interaction.Builder(123L, 456L, "click")
                                             .rating(5.0)
                                             .timestamp(Instant.now())
                                             .build();
    
  3. 最终一锤定音build() 方法是整个过程的最后一步。它会调用 Interaction 的私有构造函数,并将 Builder 自己(this)传进去。Interaction 的构造函数会从 Builder 对象中“拷贝”所有数据到自己的 final 字段里。一旦 build() 完成,一个完整的、不可变的 Interaction 对象就诞生了,而那个临时的 Builder 助手就可以功成身退了。

我们为什么需要 Builder?

总结一下,我现在爱上它的理由:

  1. 极高的可读性 .userId(123L) 这种写法,参数的意义一目了然,代码即是文档,再也不怕把参数顺序搞错了。
  2. 优雅处理可选参数:对于可选参数,比如 rating,我需要就调用 .rating(),不需要就不调用,非常灵活,彻底告别了那一长串的构造函数。
  3. 保证对象的不变性 (Immutability):这是最重要的一点!Interaction 对象没有 setter,字段都是 final 的,一旦通过 build() 创建出来,就是线程安全的,可以放心地在系统里传递,再也不用担心它的状态被意外修改。
  4. 保证对象状态的完整性:我们可以在 Builder 的构造函数里强制传入所有必填项,在 build() 方法里还可以增加更复杂的校验逻辑。这确保了我们永远不会得到一个“半成品”对象,只要对象被创建出来,它的状态就是有效和完整的。

虽然写 Builder 模式在初期会增加一些代码量,但它换来的是代码的可维护性、可读性和健壮性的大幅提升。对于那些拥有超过3、4个参数的复杂对象,尤其是数据传输对象(DTO),这种投入绝对是物超所值的。

自从我领会了 Builder 模式的妙处,基本上项目中所有复杂的数据对象都用它来构建了。不知道大家在项目中还有没有发现其他特别好用的设计模式?欢迎留言分享一下!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值