你听说了吗?用建造者模式能终结80%的空指针?

本文介绍了建造者模式的概念,使用场景以及如何通过代码实现。通过KFC套餐的例子展示了建造者模式如何将复杂对象的构建与表示分离,使得创建过程可定制。文章提供了高配和低配电脑的建造者实现,并通过导演类控制构建过程,实现了不同配置电脑的创建。总结了建造者模式的优点,如解耦产品和创建过程,易于扩展,并对比了建造者模式与抽象工厂模式的区别。

凌晨三点,刺耳的告警声划破寂静,钉钉群里瞬间被@all轰炸。核心消息推送服务大规模NullPointerException,导致下游数十个业务全部停摆。作为“事故终结者”,我被从床上薅了起来。

代码的“犯罪现场”:一个10参数的构造函数

经过半小时的紧急排查,我们锁定了“犯罪嫌疑人”——一个平平无奇的Message对象。它的创建方式,是每个项目里都可能存在的“代码原罪”:

// 线上触发NPE的罪魁祸首
public class Message {
    private String topic;      // 消息主题 (必填)
    private Object payload;    // 消息体 (必填)
    private String businessId; // 业务ID (必填)
    private Integer priority;  // 优先级 (可选)
    private Long delayMillis;  // 延迟时间 (可选)
    private Map<String, String> properties; // 扩展属性 (可选)
    // ...还有4个可选参数

    // 一个让人望而生畏的构造函数
    public Message(String topic, Object payload, String businessId, Integer priority, Long delayMillis, Map<String, String> properties, ...) {
        this.topic = topic;
        this.payload = payload;
        // ...令人眼花的赋值
    }
}

// 调用方代码
// 新来的实习生,在黑暗中摸索,漏掉了第二个参数payload
Message message = new Message("ORDER_PAID", null, "2025090412345", 1, 0L, ...); 
// 后续代码在 message.getPayload().toString() 时,NPE原地爆炸

看到了吗?一个包含了10个参数的构造函数,就像一个布满了引线的炸弹。调用者稍有不慎,传个null、颠倒一下顺序,生产环境就得抖三抖。

有人会说:“可以用JavaBean模式,提供一个无参构造和一堆setter啊!” 这更糟,它会让一个对象在构建完成前,长期处于一种“半成品”的残缺状态。你无法保证调用者会不会忘了调用某个重要的set方法。

快速止血:Lombok @Builder,三分钟的速效救心丸

在事故现场,我们需要最快的解决方案。没时间重构,Lombok的@Builder就是我们的速效救心丸。

import lombok.Builder;
import lombok.Value;

@Value // @Value = final字段 + Getter + toString/equals/hashCode + 全参构造
@Builder
public class Message {
    String topic;
    Object payload;
    String businessId;
    Integer priority;
    Long delayMillis;
    Map<String, String> properties;
}

// 调用代码,焕然一新
Message message = Message.builder()
    .topic("ORDER_PAID")
    .payload(orderInfo)
    .businessId("2025090412345")
    .priority(1)
    .build();

链式调用、语义清晰、参数可选。@Builder让代码从“猜谜游戏”变成了“填空题”,可读性和安全性瞬间拉满。事故暂时得以控制。但是,作为一名有追求的架构师,我看到了这颗“语法糖”背后更深层次的问题。

为什么OkHttp和Guava坚持手写Builder?

Lombok虽好,但它帮你隐藏了细节,也可能麻痹你的风险意识。在复盘会上,我提出了一个问题:“既然@Builder如此方便,为什么像OkHttp、Guava这些顶级库,还在坚持手动编写Builder?”

答案直指企业级开发的两个核心:强校验不可变性

Lombok生成的Builder,其build()方法默认只是一个简单的new Message(...)调用。但一个真正健壮的Builder,它的build()方法应该是对象的“最终守护者”。

让我们亲手打造一个“企业级”的Builder,看看它到底强在哪里:

import com.google.common.base.Preconditions;
import java.util.Map;

public final class Message { // 1. class为final,杜绝继承
    // 2. 所有字段为final,一旦创建,永不可变
    private final String topic;
    private final Object payload;
    private final String businessId;
    private final Integer priority;
    private final Long delayMillis;
    private final Map<String, String> properties;

    // 3. 构造函数私有,只能通过Builder创建
    private Message(MessageBuilder builder) {
        this.topic = builder.topic;
        this.payload = builder.payload;
        this.businessId = builder.businessId;
        this.priority = builder.priority;
        this.delayMillis = builder.delayMillis;
        this.properties = builder.properties;
    }

    // public getters...

    public static MessageBuilder builder() {
        return new MessageBuilder();
    }

    public static final class MessageBuilder {
        private String topic;
        private Object payload;
        private String businessId;
        // ...其他字段

        private MessageBuilder() {}

        public MessageBuilder topic(String topic) {
            this.topic = topic;
            return this;
        }

        public MessageBuilder payload(Object payload) {
            this.payload = payload;
            return this;
        }

        public MessageBuilder businessId(String businessId) {
            this.businessId = businessId;
            return this;
        }

        // ...其他链式方法

        // 4. build()方法是校验和创建的最后关卡!
        public Message build() {
            // 使用Guava的Preconditions进行防御性校验
            Preconditions.checkNotNull(topic, "topic cannot be null");
            Preconditions.checkNotNull(payload, "payload cannot be null");
            Preconditions.checkNotNull(businessId, "businessId cannot be null");
            return new Message(this);
        }
    }
}

看到区别了吗?这个手写的Builder,才是真正的“军火库”级别:

  1. 绝对不可变Message类和其所有字段都是final的,构造函数私有。一旦创建,它就是线程安全的“金刚不坏之身”。
  2. 前置防御build()方法成为了对象的“准生证”。任何不满足条件的构建尝试,都会在第一时间被Preconditions.checkNotNull拦截,以IllegalStateException的形式直接失败,而不是等到运行时才爆一个NPE

这就是Lombok无法轻易给你的——深入业务逻辑的、滴水不漏的构建期防御

顶级框架如何将Builder玩出花?

纸上谈兵终觉浅,我们去开源的“紫禁之巅”看看,大师们是如何运用这把神兵的。

OkHttp的 Request.Builder:网络请求的“安全带”

  • 坐标: okhttp3.Request.Builder
  • 精髓: OkHttp的Request对象是完全不可变的。它的Builderbuild()方法里做了大量的校验,比如URL不能为空、格式必须正确等。你永远无法通过OkHttp的Builder创建一个“非法”的请求对象。这为OkHttp的稳定性和线程安全性立下了汗马功劳。

Guava的ImmutableList.Builder:不可变集合的“铸造厂”

  • 坐标: com.google.common.collect.ImmutableList.Builder
  • 精髓: 当你调用ImmutableList.builder().add("a").add("b").build()时,Guava的Builder不仅是在收集元素,它还在内部优化存储结构。最终build()时,它会根据元素数量返回一个最高效的、内存紧凑的不可变ImmutableList实现。这是将构建过程的复杂性完美封装的典范。

Spring的BeanDefinitionBuilder:IoC容器的“蓝图设计师”

  • 坐标: org.springframework.beans.factory.support.BeanDefinitionBuilder
  • 精髓: 在Spring的编程模型中,你需要动态注册一个Bean时,就要用到它。BeanDefinitionBuilder用流畅的API,让你能像搭乐高一样,精确定义Bean的class、作用域、依赖关系等,而无需关心底层AbstractBeanDefinition的复杂继承体系和属性。它把一个极其繁琐的配置过程,变成了一套优雅的链式调用。

你的Builder,是卫兵还是摆设?

  1. 告别构造地狱:当构造函数参数超过3个时,请立刻、马上考虑使用Builder模式。
  2. 拥抱Lombok,但保持清醒@Builder是优秀的起点,但对于核心领域模型,请考虑手写Builder以加入校验逻辑和确保不可变性。
  3. 校验逻辑必须右移:将所有参数合法性校验,全部收敛到build()方法中,让它成为创建有效对象的唯一出口。
  4. Builder的最佳伴侣是final:将你的目标对象设计为不可变的,这会让你在并发编程的世界里睡得更安稳。

代码告诉你“怎么做”,而优雅的设计,则告诉你“为什么”。你的Builder,就是你对“为什么”的最好回答。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CV大魔王

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值