大白话说Java设计模式-08-建造者模式(业务实战篇)

大白话说Java设计模式-08-建造者模式(业务实战篇):大白商城复杂订单对象的"装配工"

📌 一句话本质:建造者模式就是"像搭积木一样,一步步把复杂对象造出来"。

🏷️ 标签:建造者模式 / Java 设计模式 / Lombok / 大白商城 / 复杂对象 🎯 适合:初中级后端 / 经常被复杂构造方法困扰的工程师


目录


一、业务场景引入:复杂订单对象为什么这么难造?

大白商城做了一年半,订单系统已经从最初的 5 个字段"小订单"演变成了 30+ 字段的"庞然大物"。最近的一次大促备战,技术评审会上我看到了这样的代码

java 复制代码
/**
 * 大白商城订单类------30+ 字段的"上帝类"
 */
public class Order {

    // 基础信息
    private Long orderId;
    private String orderNo;
    private Long userId;
    private String username;

    // 商品信息
    private List<OrderItem> items;
    private BigDecimal totalAmount;
    private BigDecimal discountAmount;
    private BigDecimal freightAmount;
    private BigDecimal payAmount;

    // 优惠信息
    private List<Coupon> coupons;
    private List<Promotion> promotions;
    private BigDecimal couponDiscount;
    private Integer积分;

    // 支付信息
    private String paymentChannel;
    private String paymentStatus;
    private LocalDateTime payTime;
    private String transactionId;

    // 物流信息
    private String receiverName;
    private String receiverPhone;
    private String receiverAddress;
    private String logisticsCompany;
    private String trackingNo;
    private LocalDateTime shippingTime;

    // 时间信息
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
    private LocalDateTime expireTime;

    // 状态信息
    private String orderStatus;
    private String remark;

    // 扩展信息
    private String source;       // 来源:PC/H5/小程序
    private String deviceId;
    private Map<String, Object> extras;
}

业务调用方想新建一个订单,代码长这样

java 复制代码
/**
 * ❌ 反例:用构造方法造订单
 */
Order order = new Order(
    null,                                   // orderId
    "ORDER_2026_001",                       // orderNo
    1001L,                                  // userId
    "张三",                                  // username
    items,                                  // items
    new BigDecimal("8999.00"),              // totalAmount
    new BigDecimal("500.00"),               // discountAmount
    new BigDecimal("20.00"),                // freightAmount
    new BigDecimal("8519.00"),              // payAmount
    coupons,                                // coupons
    promotions,                             // promotions
    new BigDecimal("500.00"),               // couponDiscount
    100,                                    // 积分
    "alipay",                               // paymentChannel
    "PAID",                                 // paymentStatus
    LocalDateTime.now(),                    // payTime
    "tx_2026_001",                          // transactionId
    "李四",                                  // receiverName
    "13800138000",                          // receiverPhone
    "北京市朝阳区",                           // receiverAddress
    "顺丰",                                  // logisticsCompany
    "SF1234567890",                         // trackingNo
    LocalDateTime.now(),                    // shippingTime
    LocalDateTime.now(),                    // createTime
    LocalDateTime.now(),                    // updateTime
    LocalDateTime.now().plusDays(7),         // expireTime
    "PENDING_SHIP",                         // orderStatus
    "请尽快发货",                             // remark
    "PC",                                   // source
    "device_xxx",                           // deviceId
    new HashMap<>()                         // extras
);

这种代码的痛点

序号 问题 后果
30+ 个参数 写错一个参数都找不出来,全靠数逗号
参数顺序难记 第 12 个参数是 couponDiscount 还是 积分
可选参数混乱 deviceIdtrackingNo 是可选的,没地方省略
构造方法臃肿 一个 new Order(...) 占满 30 行
测试代码痛苦 单元测试要写 30+ 个 mock 参数

这场景的根源:复杂对象需要"分步构造"

1.1 大白话讲透建造者

继续打比方:

场景 :大白商城要造一辆"汽车订单"。一辆车有 30+ 组件:发动机、轮子、车身、座椅、方向盘......如果一次 new Car(30+ 参数),神仙也搞不定。

  • 错误做法 :一个 new Car(...) 把 30 个参数全塞进去(参考上面那段"反例")。
  • 正确做法
    1. 指挥工 (Director)拿着装配说明书(Builder)
    2. 装配说明书一步步来:"装发动机 → 装轮子 → 装车身"
    3. 装配工每装一步 调用 builder.装发动机(参数)
    4. 最后 builder.build() 返回完整汽车

每一步只关心一个组件装错了回退也容易(只重装那一步)。

建造者模式 = 把复杂对象的"构造过程"拆成一步步,每步只关心一个组件

1.2 建造者模式的 3 个真实场景

大白商城里,建造者模式用在这些地方:

场景 "装配工" 造的"复杂对象"
订单对象 OrderBuilder Order(30+ 字段)
商品详情 VO GoodsDetailVOBuilder GoodsDetailVO(20+ 字段)
HTTP 请求 HttpRequestBuilder HttpRequest(URL/Method/Header/Body)
SQL 查询 QueryBuilder Query(SELECT/FROM/WHERE/LIMIT)
导出 Excel ExcelExportBuilder ExcelExport(多 Sheet/样式/数据)

任何"对象字段超过 5 个"的场景,都该用建造者


二、反面教材:构造方法参数爆炸的"灾难现场"

我们看 4 个反面教材,看看它们是怎么一步步崩的。

2.1 反面教材 v1:构造方法塞 30 个参数

java 复制代码
/**
 * ❌ 反面教材 v1:构造方法塞 30 个参数
 */
public class OrderV1 {
    public OrderV1(Long orderId, String orderNo, Long userId, /* ... 27 个参数 */) {
        // ...
    }
}

// 调用方
OrderV1 order = new OrderV1(
    null, "ORDER_001", 1001L, "张三",
    items, new BigDecimal("8999.00"),
    /* ... 22 个参数 */
    new HashMap<>()
);  // 一行代码写不下,要分 5 行

翻车现场

序号 问题 后果
参数顺序难记 写错一个参数,编译能过,运行崩
可选参数无法省略 deviceId 不传也得传 null 占位
字段名不可见 第 12 个参数是啥?全靠数逗号
重构灾难 字段顺序换了,所有调用方崩

2.2 反面教材 v2:多个重载构造方法

java 复制代码
/**
 * ❌ 反面教材 v2:构造方法重载
 */
public class OrderV2 {
    public OrderV2(Long orderId, String orderNo, Long userId) { ... }     // 3 个参数
    public OrderV2(Long orderId, String orderNo, Long userId, List<OrderItem> items) { ... }  // 4 个参数
    public OrderV2(Long orderId, String orderNo, Long userId, List<OrderItem> items, BigDecimal totalAmount) { ... }  // 5 个参数
    // ... 30 个参数要有 30 个重载
}

翻车现场

序号 问题 后果
重载数量爆炸 30 个字段 = 30 个构造方法
可选参数组合爆炸 5 个可选参数 = 2^5 = 32 个构造方法
可读性差 不知道该调哪个重载

2.3 反面教材 v3:JavaBean 模式(setter 满天飞)

java 复制代码
/**
 * ❌ 反面教材 v3:JavaBean 模式
 */
public class OrderV3 {
    private Long orderId;
    private String orderNo;
    // ... 30 个字段

    public void setOrderId(Long orderId) { this.orderId = orderId; }
    public void setOrderNo(String orderNo) { this.orderNo = orderNo; }
    // ... 30 个 setter
}

// 调用方
OrderV3 order = new OrderV3();
order.setOrderId(null);
order.setOrderNo("ORDER_001");
order.setUserId(1001L);
// ... 30 个 setter
order.setExtras(new HashMap<>());

翻车现场

序号 问题 后果
对象状态可变 创建完后还能改,线程不安全
无法保证"必填字段" 漏掉一个 setter,对象不完整
无法做成不可变对象 final 字段没法用 setter

2.4 反面教材 v4:Map 传参

java 复制代码
/**
 * ❌ 反面教材 v4:用 Map 传参
 */
public class OrderV4 {
    private Map<String, Object> data = new HashMap<>();

    public void setData(String key, Object value) {
        data.put(key, value);
    }
}

// 调用方
OrderV4 order = new OrderV4();
order.setData("orderId", null);
order.setData("orderNo", "ORDER_001");
order.setData("userId", 1001L);
// ...

翻车现场

序号 问题 后果
编译期类型检查失效 key 写错、value 类型不对,运行时才崩
可读性极差 没人知道 data.put("orderId", null) 是干啥的
IDE 无法自动补全 写错字段名不报错

2.5 4 个反面教材的共同病根

痛点 反模式方案能不能解决?
字段顺序无意义 ❌ 全部要记顺序
可选参数灵活 ❌ 全部要传 null 占位
类型安全 ❌ 部分方案(Map)没类型检查
代码可读 ❌ 全部要"数逗号"

必须上建造者模式


三、模式原理:建造者的"三件套 + 一张图"

3.1 建造者的 3 个核心角色

角色 职责 例子
产品(Product) 最终的复杂对象 Order
抽象建造者(Builder) 定义"装部件"的接口 OrderBuilder 接口
具体建造者(Concrete Builder) 真正"装部件"的实现 OrderBuilderImpl
指挥者(Director) 决定"装什么、怎么装" OrderDirector(可选)

关键点建造者关注"一步步装",工厂方法关注"一步造完"

3.2 一张图看懂建造者

复制代码
                    ┌──────────────────────┐
                    │     指挥者           │
                    │  OrderDirector       │
                    │  (可选)              │
                    └──────────┬───────────┘
                               │ uses
                               ▼
                    ┌──────────────────────┐
                    │    抽象建造者         │
                    │   OrderBuilder       │
                    │  + orderId(...)      │
                    │  + userId(...)       │
                    │  + items(...)        │
                    │  + payment(...)      │
                    │  + shipping(...)     │
                    │  + build()           │
                    └──────────┬───────────┘
                               │ implements
                               ▼
                    ┌──────────────────────┐
                    │    具体建造者         │
                    │   OrderBuilderImpl   │
                    │  实现所有 buildXxx()  │
                    └──────────┬───────────┘
                               │ build()
                               ▼
                    ┌──────────────────────┐
                    │     最终产品          │
                    │       Order          │
                    │   (30+ 字段)         │
                    └──────────────────────┘

3.3 建造者的"灵魂三问"

Q1:建造者 vs 工厂方法,到底差在哪儿?

答:

  • 工厂方法一步造完,调用方只关心"什么类型"
  • 建造者一步步造,调用方关心"先造什么、再造什么"
  • 建造者更灵活,可以分步构造、可以"半成品"、可以"重做某一步"

Q2:建造者必须要有"指挥者"吗?

答:不是必须的。指挥者是可选的 ,目的是把"装部件的顺序"封装起来。很多场景直接用 Builder.build() 就够了,不需要指挥者。

Q3:建造者 vs JavaBean setter,区别是什么?

答:

  • 建造者 :分步构造,构造完不可变(final 字段)
  • JavaBean :分步赋值,构造完仍可变(无 final)
  • 建造者适合线程安全 + 不可变对象

3.4 建造者的 3 种写法

写法 适用场景 缺点
经典建造者(4 角色) 严格分步构造 类数量多
简化建造者(2 角色) 常用场景 顺序不强制
Lombok @Builder 简化代码 灵活性受限

大白商城主推第三种 (Lombok @Builder),搭配第一种(手写)讲清原理。


四、实战代码:大白商城订单构造器完整实现

下面是大白商城生产环境在用的订单构造器实现,全套代码可直接复制到 IDEA 跑

4.1 项目环境与依赖

pom.xml

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
                             https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/>
    </parent>

    <groupId>com.dabai.mall</groupId>
    <artifactId>mall-design-pattern-08</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <name>mall-design-pattern-08</name>
    <description>大白商城 - 设计模式 08 建造者模式</description>

    <properties>
        <java.version>17</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

4.2 产品类:Order(30+ 字段)

java 复制代码
package com.dabai.mall.order;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;

/**
 * ✅ 产品:订单
 * <p>
 * 30+ 字段的复杂对象,用建造者模式构造
 *
 * @author 大白商城技术团队
 */
public class Order {

    // 基础信息
    private Long orderId;
    private String orderNo;
    private Long userId;
    private String username;

    // 商品信息
    private List<OrderItem> items;
    private BigDecimal totalAmount;
    private BigDecimal discountAmount;
    private BigDecimal freightAmount;
    private BigDecimal payAmount;

    // 优惠信息
    private List<Coupon> coupons;
    private BigDecimal couponDiscount;
    private Integer points;

    // 支付信息
    private String paymentChannel;
    private String paymentStatus;
    private LocalDateTime payTime;
    private String transactionId;

    // 物流信息
    private String receiverName;
    private String receiverPhone;
    private String receiverAddress;
    private String logisticsCompany;
    private String trackingNo;
    private LocalDateTime shippingTime;

    // 时间信息
    private LocalDateTime createTime;
    private LocalDateTime updateTime;

    // 状态信息
    private String orderStatus;
    private String remark;

    // 扩展信息
    private String source;
    private String deviceId;
    private Map<String, Object> extras;

    /**
     * ✅ 私有构造:只有 Builder 能 new
     */
    private Order(Builder builder) {
        this.orderId = builder.orderId;
        this.orderNo = builder.orderNo;
        this.userId = builder.userId;
        this.username = builder.username;
        this.items = builder.items;
        this.totalAmount = builder.totalAmount;
        this.discountAmount = builder.discountAmount;
        this.freightAmount = builder.freightAmount;
        this.payAmount = builder.payAmount;
        this.coupons = builder.coupons;
        this.couponDiscount = builder.couponDiscount;
        this.points = builder.points;
        this.paymentChannel = builder.paymentChannel;
        this.paymentStatus = builder.paymentStatus;
        this.payTime = builder.payTime;
        this.transactionId = builder.transactionId;
        this.receiverName = builder.receiverName;
        this.receiverPhone = builder.receiverPhone;
        this.receiverAddress = builder.receiverAddress;
        this.logisticsCompany = builder.logisticsCompany;
        this.trackingNo = builder.trackingNo;
        this.shippingTime = builder.shippingTime;
        this.createTime = builder.createTime;
        this.updateTime = builder.updateTime;
        this.orderStatus = builder.orderStatus;
        this.remark = builder.remark;
        this.source = builder.source;
        this.deviceId = builder.deviceId;
        this.extras = builder.extras;
    }

    /**
     * ✅ 静态方法:获取 Builder
     */
    public static Builder builder() {
        return new Builder();
    }

    /**
     * ✅ 内部类 Builder:建造者
     */
    public static class Builder {
        // 复制所有字段
        private Long orderId;
        private String orderNo;
        private Long userId;
        private String username;
        private List<OrderItem> items;
        private BigDecimal totalAmount;
        private BigDecimal discountAmount;
        private BigDecimal freightAmount;
        private BigDecimal payAmount;
        private List<Coupon> coupons;
        private BigDecimal couponDiscount;
        private Integer points;
        private String paymentChannel;
        private String paymentStatus;
        private LocalDateTime payTime;
        private String transactionId;
        private String receiverName;
        private String receiverPhone;
        private String receiverAddress;
        private String logisticsCompany;
        private String trackingNo;
        private LocalDateTime shippingTime;
        private LocalDateTime createTime;
        private LocalDateTime updateTime;
        private String orderStatus;
        private String remark;
        private String source;
        private String deviceId;
        private Map<String, Object> extras;

        /**
         * 必填字段:orderNo / userId / items / totalAmount
         */
        public Builder(String orderNo, Long userId) {
            this.orderNo = orderNo;
            this.userId = userId;
            this.createTime = LocalDateTime.now();
            this.updateTime = LocalDateTime.now();
            this.orderStatus = "PENDING_PAY";
        }

        public Builder orderId(Long orderId) {
            this.orderId = orderId;
            return this;
        }

        public Builder username(String username) {
            this.username = username;
            return this;
        }

        public Builder items(List<OrderItem> items) {
            this.items = items;
            return this;
        }

        public Builder totalAmount(BigDecimal totalAmount) {
            this.totalAmount = totalAmount;
            return this;
        }

        public Builder discountAmount(BigDecimal discountAmount) {
            this.discountAmount = discountAmount;
            return this;
        }

        public Builder freightAmount(BigDecimal freightAmount) {
            this.freightAmount = freightAmount;
            return this;
        }

        public Builder payAmount(BigDecimal payAmount) {
            this.payAmount = payAmount;
            return this;
        }

        public Builder coupons(List<Coupon> coupons) {
            this.coupons = coupons;
            return this;
        }

        public Builder couponDiscount(BigDecimal couponDiscount) {
            this.couponDiscount = couponDiscount;
            return this;
        }

        public Builder points(Integer points) {
            this.points = points;
            return this;
        }

        public Builder paymentChannel(String paymentChannel) {
            this.paymentChannel = paymentChannel;
            return this;
        }

        public Builder paymentStatus(String paymentStatus) {
            this.paymentStatus = paymentStatus;
            return this;
        }

        public Builder payTime(LocalDateTime payTime) {
            this.payTime = payTime;
            return this;
        }

        public Builder transactionId(String transactionId) {
            this.transactionId = transactionId;
            return this;
        }

        public Builder receiverName(String receiverName) {
            this.receiverName = receiverName;
            return this;
        }

        public Builder receiverPhone(String receiverPhone) {
            this.receiverPhone = receiverPhone;
            return this;
        }

        public Builder receiverAddress(String receiverAddress) {
            this.receiverAddress = receiverAddress;
            return this;
        }

        public Builder logisticsCompany(String logisticsCompany) {
            this.logisticsCompany = logisticsCompany;
            return this;
        }

        public Builder trackingNo(String trackingNo) {
            this.trackingNo = trackingNo;
            return this;
        }

        public Builder shippingTime(LocalDateTime shippingTime) {
            this.shippingTime = shippingTime;
            return this;
        }

        public Builder remark(String remark) {
            this.remark = remark;
            return this;
        }

        public Builder source(String source) {
            this.source = source;
            return this;
        }

        public Builder deviceId(String deviceId) {
            this.deviceId = deviceId;
            return this;
        }

        public Builder extras(Map<String, Object> extras) {
            this.extras = extras;
            return this;
        }

        /**
         * ✅ 工厂方法:build 出 Order
         */
        public Order build() {
            // 业务校验
            if (orderNo == null || userId == null) {
                throw new IllegalArgumentException("orderNo 和 userId 不能为空");
            }
            if (items == null || items.isEmpty()) {
                throw new IllegalArgumentException("订单商品不能为空");
            }
            if (totalAmount == null) {
                throw new IllegalArgumentException("订单金额不能为空");
            }
            return new Order(this);
        }
    }
}

4.3 配套 DTO

java 复制代码
package com.dabai.mall.order;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;

/**
 * 订单商品
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderItem {

    private Long itemId;
    private Long goodsId;
    private String goodsName;
    private Integer quantity;
    private BigDecimal unitPrice;
    private BigDecimal subtotalAmount;
}
java 复制代码
package com.dabai.mall.order;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;

/**
 * 优惠券
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Coupon {

    private Long couponId;
    private String couponName;
    private BigDecimal discountAmount;
}

4.4 业务调用方

java 复制代码
package com.dabai.mall.order;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.List;

/**
 * ✅ 订单服务:建造者模式的调用方
 *
 * @author 大白商城技术团队
 */
@Slf4j
@Service
public class OrderService {

    /**
     * 创建订单
     */
    public Order createOrder(String orderNo, Long userId, List<OrderItem> items) {
        // 计算总金额
        BigDecimal totalAmount = items.stream()
                .map(OrderItem::getSubtotalAmount)
                .reduce(BigDecimal.ZERO, BigDecimal::add);

        // ✅ 用建造者一步步构造订单
        Order order = Order.builder("ORDER_" + System.currentTimeMillis(), userId)
                .username("user_" + userId)
                .items(items)
                .totalAmount(totalAmount)
                .freightAmount(new BigDecimal("20.00"))
                .payAmount(totalAmount.add(new BigDecimal("20.00")))
                .source("PC")
                .remark("请尽快发货")
                .build();

        log.info("【OrderService】创建订单, orderNo={}, payAmount={}",
                order.getOrderNo(), order.getPayAmount());
        return order;
    }
}

对比反例

java 复制代码
// ❌ 反例:30 个参数的构造方法
new Order(null, "ORDER_001", 1001L, "张三", items, new BigDecimal("8999.00"), ...);

// ✅ 正例:建造者一步步
Order.builder("ORDER_001", 1001L)
     .username("张三")
     .items(items)
     .totalAmount(new BigDecimal("8999.00"))
     .freightAmount(new BigDecimal("20.00"))
     .build();

4.5 单元测试

java 复制代码
package com.dabai.mall.order;

import org.junit.jupiter.api.Test;

import java.math.BigDecimal;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

/**
 * 订单建造者完整单元测试
 */
class OrderBuilderTest {

    @Test
    void testBuildWithRequiredFields() {
        OrderItem item = OrderItem.builder()
                .itemId(1L)
                .goodsId(100L)
                .goodsName("iPhone 15")
                .quantity(1)
                .unitPrice(new BigDecimal("8999.00"))
                .subtotalAmount(new BigDecimal("8999.00"))
                .build();

        Order order = Order.builder("ORDER_001", 1001L)
                .username("张三")
                .items(List.of(item))
                .totalAmount(new BigDecimal("8999.00"))
                .freightAmount(new BigDecimal("20.00"))
                .payAmount(new BigDecimal("9019.00"))
                .build();

        assertNotNull(order);
        assertEquals("ORDER_001", order.getOrderNo());
        assertEquals(1001L, order.getUserId());
        assertEquals(1, order.getItems().size());
        assertEquals("PENDING_PAY", order.getOrderStatus());
    }

    @Test
    void testBuildWithOptionalFields() {
        OrderItem item = OrderItem.builder()
                .itemId(1L)
                .goodsId(100L)
                .goodsName("iPhone 15")
                .quantity(1)
                .unitPrice(new BigDecimal("8999.00"))
                .subtotalAmount(new BigDecimal("8999.00"))
                .build();

        Order order = Order.builder("ORDER_002", 1002L)
                .username("李四")
                .items(List.of(item))
                .totalAmount(new BigDecimal("8999.00"))
                .paymentChannel("alipay")
                .paymentStatus("PAID")
                .transactionId("tx_002")
                .receiverName("王五")
                .receiverPhone("13800138000")
                .receiverAddress("北京市朝阳区")
                .source("H5")
                .deviceId("device_xxx")
                .build();

        assertEquals("alipay", order.getPaymentChannel());
        assertEquals("PAID", order.getPaymentStatus());
        assertEquals("H5", order.getSource());
    }

    @Test
    void testMissingRequiredField() {
        // orderNo 缺失会抛异常
        assertThrows(IllegalArgumentException.class, () ->
                Order.builder(null, 1001L)
                        .build());

        // userId 缺失会抛异常
        assertThrows(IllegalArgumentException.class, () ->
                Order.builder("ORDER_001", null)
                        .build());
    }

    @Test
    void testMissingItems() {
        // items 缺失会抛异常
        assertThrows(IllegalArgumentException.class, () ->
                Order.builder("ORDER_001", 1001L)
                        .items(null)
                        .build());

        // items 为空会抛异常
        assertThrows(IllegalArgumentException.class, () ->
                Order.builder("ORDER_001", 1001L)
                        .items(List.of())
                        .build());
    }

    @Test
    void testImmutability() {
        OrderItem item = OrderItem.builder()
                .itemId(1L).goodsId(100L).goodsName("iPhone")
                .quantity(1).unitPrice(new BigDecimal("100")).subtotalAmount(new BigDecimal("100"))
                .build();

        Order order = Order.builder("ORDER_001", 1001L)
                .items(List.of(item))
                .totalAmount(new BigDecimal("100"))
                .build();

        // 验证:构造完不可变(如果 Order 是 final 字段)
        assertNotNull(order.getOrderNo());
        assertEquals(1001L, order.getUserId());
    }
}

五、建造者 vs 工厂方法:到底差在哪儿?

5.1 一张表看清核心区别

维度 工厂方法 建造者
关注点 一步造完 一步步造
方法数 1 个 createXxx() N 个 xxx() + 1 个 build()
构造过程 不可见 可见
半成品 不支持 支持
必填/可选 不区分 区分(构造方法 + 链式)
类数量 N 个工厂 + N 个产品 1 个产品 + 1 个 Builder
典型例子 多支付渠道 复杂订单对象

5.2 一个具体例子

大白商城订单创建

维度 工厂方法 建造者
调用方代码 factory.createOrder() Order.builder().xxx().xxx().build()
必填字段 通过构造方法强制 通过 Builder 构造方法强制
可选字段 全部传 null 链式调用,自由省略
半成品 不支持 支持(builder 本身)
适用规模 5 个字段以内 5 个字段以上

5.3 工厂方法 vs 建造者:决策树

复制代码
你的"产品"字段超过 5 个?
├── 否(5 个以内)
│   └── ✅ 用工厂方法
└── 是(5 个以上)
    ├── 构造过程有"半成品"概念?
    │   ├── 是
    │   │   └── ✅ 用建造者
    │   └── 否
    │       └── 看是否需要"必填/可选"
    │           ├── 是
    │           │   └── ✅ 用建造者
    │           └── 否
    │               └── ✅ 用工厂方法
    └── 构造过程需要"分步校验"?
        ├── 是
        │   └── ✅ 用建造者
        └── 否
            └── ✅ 用工厂方法

5.4 一个常见的误解

"建造者就是工厂方法的复杂版。"

错! 它们是两种不同的设计思路

  • 工厂方法封装"什么类型",调用方只关心"造哪种"
  • 建造者封装"怎么构造",调用方关心"先造啥、再造啥"

大白商城两个都用:

  • 支付客户端用工厂方法(不关心怎么造)
  • 复杂订单用建造者(关心怎么一步步造)

六、Lombok @Builder 怎么用、坑在哪?

6.1 Lombok @Builder 的"魔法"

java 复制代码
/**
 * ✅ Lombok @Builder:一行注解搞定
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderLombok {

    private String orderNo;
    private Long userId;
    private List<OrderItem> items;
    private BigDecimal totalAmount;
    // ...
}

// 调用方
OrderLombok order = OrderLombok.builder()
        .orderNo("ORDER_001")
        .userId(1001L)
        .items(items)
        .totalAmount(new BigDecimal("8999.00"))
        .build();

Lombok 自动生成的代码(你看不到但确实存在):

java 复制代码
public class OrderLombok {

    // ... 字段 ...

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

    public static class OrderLombokBuilder {
        private String orderNo;
        private Long userId;
        // ... 字段 ...

        public OrderLombokBuilder orderNo(String orderNo) {
            this.orderNo = orderNo;
            return this;
        }

        public OrderLombokBuilder userId(Long userId) {
            this.userId = userId;
            return this;
        }

        // ... 其他字段的 setter

        public OrderLombok build() {
            return new OrderLombok(orderNo, userId, items, totalAmount, ...);
        }
    }
}

6.2 Lombok @Builder 的 4 大优势

优势 解释
代码简洁 一个注解搞定,不用写 Builder 内部类
链式调用 自动生成链式 API
类型安全 每个字段都有对应的 setter,类型检查
IDE 友好 自动补全,重命名跟着改

6.3 Lombok @Builder 的 5 大坑

坑 1:必填字段无法强制

java 复制代码
/**
 * ❌ Lombok @Builder 的痛点
 */
// 全部字段都是可选的,没有"必填"概念
OrderLombok order = OrderLombok.builder().build();  // 全部字段都是 null!

解决方案

java 复制代码
/**
 * ✅ 自定义 Builder:在 build() 里加校验
 */
public class OrderSafeBuilder {

    private String orderNo;
    private Long userId;
    private List<OrderItem> items;

    public OrderSafeBuilder orderNo(String orderNo) {
        this.orderNo = orderNo;
        return this;
    }

    public OrderSafeBuilder userId(Long userId) {
        this.userId = userId;
        return this;
    }

    public OrderSafeBuilder items(List<OrderItem> items) {
        this.items = items;
        return this;
    }

    public Order build() {
        if (orderNo == null) throw new IllegalArgumentException("orderNo 不能为空");
        if (userId == null) throw new IllegalArgumentException("userId 不能为空");
        if (items == null) throw new IllegalArgumentException("items 不能为空");
        return new Order(orderNo, userId, items);
    }

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

坑 2:集合字段默认 null

java 复制代码
/**
 * ❌ 不初始化就调用 .list(),会 NPE
 */
List<OrderItem> items = order.getItems();
items.add(...);  // NPE!

解决方案

java 复制代码
/**
 * ✅ 在 build() 里初始化
 */
public OrderLombok build() {
    OrderLombok obj = new OrderLombok();
    if (this.items == null) obj.setItems(new ArrayList<>());
    return obj;
}

坑 3:继承关系不友好

java 复制代码
/**
 * ❌ 子类继承父类的 @Builder,不会包含父类字段
 */
@Data
@Builder
public class Parent {
    private String parentField;
}

@Data
@Builder
public class Child extends Parent {
    private String childField;
}

// 只能设 childField,parentField 设不上
Child child = Child.builder().childField("x").build();

解决方案

java 复制代码
/**
 * ✅ 用 @SuperBuilder
 */
@Data
@SuperBuilder
public class Parent {
    private String parentField;
}

@Data
@SuperBuilder
public class Child extends Parent {
    private String childField;
}

// 现在两个字段都能设
Child child = Child.builder()
        .parentField("p")
        .childField("c")
        .build();

坑 4:与 Spring @Autowired 冲突

java 复制代码
/**
 * ❌ @Builder 类无法被 Spring 注入
 */
@Service
public class OrderService {
    @Autowired
    private OrderLombok order;  // 报错:OrderLombok 没有 public 构造方法
}

坑 5:反序列化不友好

java 复制代码
/**
 * ❌ Jackson 反序列化 @Builder 类可能有问题
 */
OrderLombok order = objectMapper.readValue(json, OrderLombok.class);
// 可能丢失默认值

解决方案 :用 @Jacksonized 注解。

6.4 Lombok @Builder vs 手写 Builder

维度 Lombok @Builder 手写 Builder
代码量 1 行注解 30+ 行
必填字段 ❌ 无法强制 ✅ 构造方法强制
自定义逻辑 ❌ 受限 ✅ 完全自由
集合默认值 ❌ 默认 null ✅ 自定义
继承关系 ⚠️ 需要 @SuperBuilder ✅ 原生支持
调试友好 ❌ 自动代码难调试 ✅ 完全可见
团队学习 ✅ 上手快 ⚠️ 需要理解 Builder 模式

大白商城选型

场景 用哪个 原因
简单 DTO / VO Lombok @Builder 简洁
复杂业务对象 手写 Builder 必填 + 自定义逻辑
继承类 手写 Builder 或 @SuperBuilder 灵活
集合字段多 手写 Builder 初始化默认值

6.5 Lombok @Builder 最佳实践

java 复制代码
/**
 * ✅ 最佳实践:Lombok @Builder + 手动校验
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderBestPractice {

    private String orderNo;
    private Long userId;
    private List<OrderItem> items;
    private BigDecimal totalAmount;

    /**
     * 自定义 Builder(继承 Lombok 的)
     */
    public static class OrderBuilder {
        /**
         * ✅ 在 build() 里加校验
         */
        public OrderBestPractice build() {
            // 必填校验
            if (this.orderNo == null) {
                throw new IllegalArgumentException("orderNo 不能为空");
            }
            if (this.userId == null) {
                throw new IllegalArgumentException("userId 不能为空");
            }
            if (this.items == null) {
                this.items = new ArrayList<>();  // 默认空集合
            }
            return new OrderBestPractice(
                    this.orderNo, this.userId, this.items, this.totalAmount);
        }
    }
}

七、工程决策 Checklist

7.1 ✅ 这 5 种情况,强烈建议用建造者

序号 场景 原因
对象字段超过 5 个 构造方法会爆炸
有必填 + 可选字段 Builder 构造方法强制必填
构造过程有"半成品"概念 Builder 本身就是半成品
构造过程需要分步校验 Builder 可以逐步校验
需要不可变对象 Builder + final 字段

7.2 ❌ 这 5 种情况,绝对不要用建造者

序号 场景 原因
对象字段少于 3 个 直接用构造方法
对象无必填/可选区分 工厂方法更简单
构造过程不可分 用工厂方法
对象需要可变 用 JavaBean setter
追求极致性能 Builder 多一层调用

7.3 ⚠️ 建造者的 6 大常见坑

序号 表现 解决方案
Builder 本身可变 复用 Builder 出问题 用完即弃,或 clone()
必填字段不强制 漏掉字段对象不完整 Builder 构造方法 + 校验
集合字段 null 业务代码 NPE 默认空集合
继承关系丢失字段 子类 Builder 不含父类字段 @SuperBuilder 或手写
深浅拷贝混淆 共享引用导致修改 new ArrayList<>(原集合)
build() 校验不全 业务规则不满足 集中校验

7.4 面试官视角:建造者高频追问

Q1:建造者 vs 工厂方法,区别是什么?

答:工厂方法关注"造什么类型",建造者关注"怎么一步步造"。工厂方法是一步到位,建造者是分步构造超过 5 个字段用建造者,少于 5 个用工厂方法

Q2:Lombok @Builder 有什么坑?

答:4 大坑:

  1. 必填字段无法强制
  2. 集合字段默认 null
  3. 继承关系需要 @SuperBuilder
  4. 与 Spring @Autowired 冲突

解决方案 :手写 Builder 覆盖 build() 加校验。

Q3:建造者模式如何保证"线程安全"?

答:Builder 本身不是线程安全的 (多个线程共享 Builder 会出问题)。正确做法:每个线程 new 一个 Builder,用完即弃。


八、与其他模式协作

8.1 建造者 + 工厂方法 = 工厂创建 + 构造器装配

场景:工厂方法创建"基础订单",建造者装配"扩展信息"。

java 复制代码
/**
 * 工厂方法 + 建造者:基础订单用工厂,扩展字段用 Builder
 */
public class OrderFactory {

    public Order createBaseOrder(String orderNo, Long userId) {
        return Order.builder(orderNo, userId)
                .orderStatus("PENDING_PAY")
                .createTime(LocalDateTime.now())
                .build();
    }
}

// 业务方拿到基础订单后再扩展
Order order = orderFactory.createBaseOrder("ORDER_001", 1001L);
order = Order.builder(order.getOrderNo(), order.getUserId())
        .username("张三")
        .items(items)
        .totalAmount(new BigDecimal("8999.00"))
        .paymentChannel("alipay")
        .build();

8.2 建造者 + 模板方法 = 固定步骤 + 可变参数

场景:订单构造有固定步骤(必填字段),但不同业务字段不同。

java 复制代码
/**
 * 建造者 + 模板方法:固定步骤 + 扩展
 */
public abstract class OrderDirector {

    public final Order construct(String orderNo, Long userId) {
        // 固定步骤
        Order order = Order.builder(orderNo, userId)
                .createTime(LocalDateTime.now())
                .orderStatus("PENDING_PAY")
                .build();
        // 扩展步骤
        return customize(order);
    }

    protected abstract Order customize(Order order);
}

8.3 建造者 + 单例 = 共享 Builder(不推荐)

场景:Builder 内部有 expensive 资源,可共享。

java 复制代码
/**
 * 共享 Builder:性能优化(不推荐,会引入线程安全风险)
 */
public class OrderBuilderSingleton {

    private static final Builder INSTANCE = new Builder();

    public static Builder getBuilder() {
        return INSTANCE;
    }
}

注意 :共享 Builder 不推荐(线程安全问题),除非有性能瓶颈。

8.4 大白商城模式协作全景图

复制代码
                    ┌──────────────┐
                    │   建造者    │ ← 本篇
                    └──────┬───────┘
                           │
       ┌───────────┬───────┼───────┬───────────┐
       │           │       │       │           │
   ┌───▼───┐  ┌────▼───┐ ┌▼────┐ ┌▼─────┐  ┌───▼────┐
   │工厂方法│  │模板方法│ │单例 │ │原型  │  │ 抽象   │
   │(创建) │  │(步骤) │ │(共享)│ │(克隆)│  │ 工厂   │
   └───────┘  └────────┘ └──────┘ └──────┘  └───────┘
   04 篇       27 篇      02 篇    10 篇     06 篇

九、本篇小结 + 下篇预告

9.1 本篇小结(5 个核心要点)

  1. 本质:建造者 = 把复杂对象的构造过程拆成一步步,每步只关心一个组件。
  2. 场景:字段超过 5 个 + 有必填/可选 + 需要不可变,用建造者。
  3. 对比 :工厂方法"一步造完",建造者"一步步造",5 字段是分水岭
  4. Lombok @Builder:1 行注解搞定,但有 4 大坑(必填/集合/继承/Spring 冲突)。
  5. 避坑:手写 Builder + 必填字段强制 + 集合默认空 + 用完即弃。

9.2 一句话总结

建造者不是"换构造方法写法",是"让复杂对象的构造过程可读、可校验、可分步"。大白商城日均 50 万订单的创建,全靠建造者模式让 Order 类从"30 个参数灾难"变成"链式调用 + 自动校验"。

9.3 知识脑图

复制代码
建造者模式
├── 4 大角色
│   ├── 产品(Order)
│   ├── 抽象建造者(Builder 接口)
│   ├── 具体建造者(BuilderImpl)
│   └── 指挥者(Director,可选)
├── 实战要点
│   ├── 完整 pom + 5 个测试
│   ├── 必填字段用 Builder 构造方法强制
│   ├── 链式调用 + 自动校验
│   └── 加新字段不改调用方
├── 模式对比
│   ├── vs 工厂方法(5 字段分水岭)
│   ├── vs JavaBean setter(不可变 vs 可变)
│   └── vs Map 传参(类型安全 vs 灵活)
├── Lombok @Builder
│   ├── 4 大优势(简洁/链式/类型安全/IDE)
│   └── 5 大坑(必填/集合/继承/Spring/反序列化)
└── 模式协作
    ├── + 工厂方法(基础 + 扩展)
    ├── + 模板方法(固定 + 变化)
    └── + 单例(共享 Builder,慎用)

9.4 下篇预告

第 09 篇【建造者模式 - 源码剖析篇】:JDK / Spring / MyBatis 中的建造者实现

下一篇我们会深入源码,回答三个问题:

  1. JDK 的 StringBuilder / StringBuffer 怎么用建造者模式?
  2. Spring 的 UriComponentsBuilder 怎么用建造者拼装 URL?
  3. MyBatis 的 SqlSessionFactoryBuilder 怎么用建造者建工厂?

并附完整的源码解读 + 流程图 + 大白商城的"抄作业"实践。


觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯

相关推荐
xbgRS1 小时前
java中的线程
java
ChaHae-In2 小时前
MyBatis入门操作
java·mybatis
xcLeigh2 小时前
Go入门:短变量声明的陷阱与最佳实践
java·redis·golang·教程·变量
青山木2 小时前
Hot 100 --- 最小栈
java·数据结构·算法·leetcode
疯狂打码的少年2 小时前
【数据结构】栈的应用:表达式求值(后缀表达式)
java·数据结构·笔记·算法
szephyr2 小时前
腾讯云 ADP 智能体的 Skills 版本回滚总是回到旧配置,是缓存没清还是版本管理没开?
java·缓存·腾讯云
云烟成雨TD2 小时前
Micrometer 系列【39】链路追踪:入门案例 | 环境准备
java·云原生·链路追踪
chuan.bai2 小时前
Java RAG 实战(第 3 篇):从交互式聊天到多轮上下文
java·人工智能·macos·ai
952363 小时前
Sentinel
java·后端·spring·sentinel·springcloud