大白话说Java设计模式-08-建造者模式(业务实战篇):大白商城复杂订单对象的"装配工"
📌 一句话本质:建造者模式就是"像搭积木一样,一步步把复杂对象造出来"。
🏷️ 标签:建造者模式 / Java 设计模式 / Lombok / 大白商城 / 复杂对象 🎯 适合:初中级后端 / 经常被复杂构造方法困扰的工程师
目录
- 一、业务场景引入:复杂订单对象为什么这么难造?
- 二、反面教材:构造方法参数爆炸的"灾难现场"
- [三、模式原理:建造者的"三件套 + 一张图"](#三、模式原理:建造者的"三件套 + 一张图")
- 四、实战代码:大白商城订单构造器完整实现
- [五、建造者 vs 工厂方法:到底差在哪儿?](#五、建造者 vs 工厂方法:到底差在哪儿?)
- [六、Lombok @Builder 怎么用、坑在哪?](#六、Lombok @Builder 怎么用、坑在哪?)
- [七、工程决策 Checklist](#七、工程决策 Checklist)
- 八、与其他模式协作
- [九、本篇小结 + 下篇预告](#九、本篇小结 + 下篇预告)
一、业务场景引入:复杂订单对象为什么这么难造?
大白商城做了一年半,订单系统已经从最初的 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 还是 积分? |
| ③ | 可选参数混乱 | deviceId 和 trackingNo 是可选的,没地方省略 |
| ④ | 构造方法臃肿 | 一个 new Order(...) 占满 30 行 |
| ⑤ | 测试代码痛苦 | 单元测试要写 30+ 个 mock 参数 |
这场景的根源:复杂对象需要"分步构造"。
1.1 大白话讲透建造者
继续打比方:
场景 :大白商城要造一辆"汽车订单"。一辆车有 30+ 组件:发动机、轮子、车身、座椅、方向盘......如果一次
new Car(30+ 参数),神仙也搞不定。
- 错误做法 :一个
new Car(...)把 30 个参数全塞进去(参考上面那段"反例")。- 正确做法 :
- 指挥工 (Director)拿着装配说明书(Builder)
- 装配说明书一步步来:"装发动机 → 装轮子 → 装车身"
- 装配工每装一步 调用
builder.装发动机(参数)- 最后
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 大坑:
- 必填字段无法强制
- 集合字段默认 null
- 继承关系需要
@SuperBuilder- 与 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 个核心要点)
- 本质:建造者 = 把复杂对象的构造过程拆成一步步,每步只关心一个组件。
- 场景:字段超过 5 个 + 有必填/可选 + 需要不可变,用建造者。
- 对比 :工厂方法"一步造完",建造者"一步步造",5 字段是分水岭。
- Lombok
@Builder:1 行注解搞定,但有 4 大坑(必填/集合/继承/Spring 冲突)。 - 避坑:手写 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 中的建造者实现
下一篇我们会深入源码,回答三个问题:
- JDK 的
StringBuilder/StringBuffer怎么用建造者模式? - Spring 的
UriComponentsBuilder怎么用建造者拼装 URL? - MyBatis 的
SqlSessionFactoryBuilder怎么用建造者建工厂?
并附完整的源码解读 + 流程图 + 大白商城的"抄作业"实践。
觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯