建造者模式:复杂对象的构建、链式调用、实战: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();

你看,这样调用方一眼就能看出查询条件是什么,可读性极好。

建造者模式的核心流程图

建造者模式核心流程 客户端 (Client) Builder (建造者) .orderId() .userId() .addItem() ... build() 方法 Product (最终对象) 关键点 1. 链式调用返回 this 2. build() 做参数校验 3. 产品类构造器私有 4. 可选 Director 控制流程 5. 不可变对象更安全 6. Lombok @Builder 自动生成

什么时候用建造者模式?

我总结了几条经验:

  1. 对象字段超过 4 个,且部分可选。这时候构造函数已经不太够用了。
  2. 对象创建后不希望被修改。建造者模式配合不可变对象,非常安全。
  3. 构建过程有顺序依赖。比如必须先设置 A 才能设置 B,可以在 Builder 里做状态控制。
  4. 需要生成多种表现形式。同一个 Builder 接口,不同实现可以产出不同产品。

我的建议:不要为了用模式而用模式。如果对象只有两三个字段,直接 new 或者用静态工厂方法就够了。过度设计也是种坏味道。

避坑指南

最后分享几个我踩过的坑:

  • 线程安全:Builder 本身不是线程安全的。如果多个线程共享同一个 Builder 实例,记得加锁或者每个线程 new 一个。
  • 继承问题:如果产品类有继承关系,Builder 也要跟着继承。Lombok 的 @Builder 对继承支持不太好,需要手动处理。
  • 性能损耗:每次构建都会创建 Builder 对象,如果构建频率极高(比如每秒百万次),要考虑对象创建的开销。
  • 不要滥用链式调用:链式调用虽然好看,但如果方法调用链太长(超过 10 个),反而影响可读性。

嗯,建造者模式就聊到这儿。记住一句话:当构造函数的参数列表让你想骂娘的时候,就该用建造者模式了。


公众号:蓝海资料掘金营,微信deep3321