一、为什么需要建造者模式?------从痛点出发
1. 问题场景:当对象像"乐高积木"需要组合时
想象你正在组装一台高配置电脑:
- 需要选择CPU(i7/i9)
- 需要搭配显卡(RTX 3080/4090)
- 需要确定内存大小(16G/32G)
- 还要选择硬盘(SSD 512G/1T)
如果用传统方式创建对象,代码会变成这样:
java
Computer computer = new Computer("i9-13900K", "RTX 4090", 64, "2TB SSD", ...);
当参数超过5个时,你还能分清哪个参数对应哪个部件吗?
2. 传统方式的致命缺陷(附代码对比)
致命问题一:参数顺序噩梦
java
// 两个构造方法参数顺序不同,但编译不会报错!
new User("张三", 25); // (name, age)
new User(25, "张三"); // (age, name) ❌
致命问题二:参数爆炸的构造方法
java
// 参数超过5个时,调用者会陷入参数地狱
new Order(
"ORD20230901001", // 订单号
"张三", // 用户姓名
"13800138000", // 手机号
"北京市海淀区", // 地址
new Date(), // 创建时间
2, // 商品数量
1999.00, // 总金额
"满减优惠", // 优惠类型
"顺丰快递" // 物流方式
);
致命问题三:Setter的定时炸弹
java
User user = new User();
user.setName("李四"); // 忘记设置必填项age
user.setAge(null); // 不小心设置错误类型
// 此时user对象处于不完整状态!
3. 建造者模式的救赎(代码对比)
解决方案一:分步组装就像点麦当劳套餐
java
Computer computer = new ComputerBuilder()
.chooseCpu("i9-13900K") // 第一步:选CPU
.addGpu("RTX 4090") // 第二步:加显卡
.setRam(64) // 第三步:定内存
.withStorage("2TB SSD") // 第四步:配硬盘
.build(); // 最后组装成品
解决方案二:强制完成必填项
java
public User build() {
if (name == null || age == null) {
throw new IllegalStateException("姓名和年龄是必填项!");
}
return new User(name, age, phone...);
}
解决方案三:链式调用清晰如对话
java
// 读代码就像读说明书
HttpRequest request = new HttpRequestBuilder()
.method("POST")
.url("https://api.example.com")
.header("Content-Type", "application/json")
.body("{'key':'value'}")
.timeout(5000)
.build();
真实项目中的惨痛教训:某电商系统订单创建BUG
某电商平台曾因订单参数顺序错误导致:
- 用户地址被存入商品数量字段
- 物流方式变成浮点数
- 引发大规模订单数据混乱
改用建造者模式后:
java
// 明确每个参数的语义
Order order = new OrderBuilder()
.orderId("ORD20230901001")
.userName("张三")
.shippingAddress("北京市海淀区")
.itemCount(2)
.totalAmount(1999.00)
.build();
从此再未出现参数错位问题!
建造者模式的本质 :把复杂对象的构建过程 与最终表示分离,就像汽车工厂的流水线,每个工位只负责一个部件的安装,最后质检员(build()方法)确保整车完整可用。
二、建造者模式核心角色(附UML图)
核心角色交互图
classDiagram
class Computer {
-cpu: String
-gpu: String
-ram: int
+Computer()
+getCpu() String
+getGpu() String
+getRam() int
}
class ComputerBuilder {
<>
+setCpu(String cpu) ComputerBuilder
+setGpu(String gpu) ComputerBuilder
+setRam(int ram) ComputerBuilder
+build() Computer
}
class StandardComputerBuilder {
-computer: Computer
+setCpu(String cpu) ComputerBuilder
+setGpu(String gpu) ComputerBuilder
+setRam(int ram) ComputerBuilder
+build() Computer
}
class ComputerDirector {
-builder: ComputerBuilder
+constructGamingPC() Computer
+constructOfficePC() Computer
}
Computer <.. StandardComputerBuilder : 构建
ComputerBuilder <|.. StandardComputerBuilder : 实现
ComputerDirector --> ComputerBuilder : 使用
角色详解 + 代码对照
1. 产品(Product)------你的目标对象
java
// 就像你要组装的电脑整机
public class Computer {
private String cpu;
private String gpu;
private int ram;
// 全参构造器(通常设置为私有)
public Computer(String cpu, String gpu, int ram) {
this.cpu = cpu;
this.gpu = gpu;
this.ram = ram;
}
// Getter方法省略...
}
2. 建造者(Builder)------组装说明书
java
// 定义组装步骤的接口
public interface ComputerBuilder {
// 每个方法都返回Builder自身,实现链式调用
ComputerBuilder setCpu(String cpu);
ComputerBuilder setGpu(String gpu);
ComputerBuilder setRam(int ram);
// 最终生成产品的方法
Computer build();
}
3. 具体建造者(ConcreteBuilder)------真正的组装工人
java
public class StandardComputerBuilder implements ComputerBuilder {
private String cpu;
private String gpu;
private int ram;
@Override
public ComputerBuilder setCpu(String cpu) {
this.cpu = cpu;
return this; // 返回当前对象,实现链式调用
}
// 其他set方法同理...
@Override
public Computer build() {
// 此处可添加校验逻辑
if(cpu == null || gpu == null){
throw new IllegalArgumentException("CPU和显卡是必选配件!");
}
return new Computer(cpu, gpu, ram);
}
}
4. 指挥者(Director)------流水线经理(可选)
java
public class ComputerDirector {
private final ComputerBuilder builder;
public ComputerDirector(ComputerBuilder builder) {
this.builder = builder;
}
// 封装高端游戏机的配置流程
public Computer constructGamingPC() {
return builder
.setCpu("i9-13900K")
.setGpu("RTX 4090")
.setRam(64)
.build();
}
// 封装办公电脑的配置流程
public Computer constructOfficePC() {
return builder
.setCpu("i5-12400")
.setGpu("Intel UHD 730")
.setRam(16)
.build();
}
}
指挥者的使用场景演示
java
// 当需要预定义配置方案时
ComputerBuilder builder = new StandardComputerBuilder();
ComputerDirector director = new ComputerDirector(builder);
// 快速获得预定义的两种配置
Computer gamingPC = director.constructGamingPC();
Computer officePC = director.constructOfficePC();
// 当需要自定义配置时,依然可以直接使用Builder
Computer customPC = new StandardComputerBuilder()
.setCpu("Ryzen 7 7800X")
.setGpu("RX 7900 XTX")
.setRam(32)
.build();
架构师决策点:什么时候需要指挥者?
场景特征 | 使用指挥者 | 直接使用Builder |
---|---|---|
需要固定配置模板 | ✅ | ❌ |
参数组合经常变化 | ❌ | ✅ |
多个子系统需要相同配置 | ✅ | ❌ |
希望隐藏复杂构建细节 | ✅ | ❌ |
设计建议:80%的场景不需要指挥者,但当你的系统出现重复的构建代码块时,就是引入Director的最佳时机。
三、经典应用场景举例(代码级详解)
1. 电商订单系统(不同优惠组合)
痛点:满减、折扣、赠品等多种优惠组合的灵活配置
java
// 产品类:订单
public class Order {
private String orderId; // 订单号(必填)
private String userId; // 用户ID(必填)
private BigDecimal amount; // 订单金额(必填)
private String coupon; // 优惠券(可选)
private String gift; // 赠品(可选)
// 其他10+个字段...
// 私有构造器(只能通过Builder创建)
private Order(OrderBuilder builder) {
this.orderId = builder.orderId;
this.userId = builder.userId;
this.amount = builder.amount;
this.coupon = builder.coupon;
this.gift = builder.gift;
}
// 建造者(内部类)
public static class OrderBuilder {
// 必填字段通过构造函数传入
public OrderBuilder(String orderId, String userId, BigDecimal amount) {
this.orderId = orderId;
this.userId = userId;
this.amount = amount;
}
// 可选字段通过链式方法设置
public OrderBuilder withCoupon(String coupon) {
this.coupon = coupon;
return this;
}
public OrderBuilder addGift(String gift) {
this.gift = gift;
return this;
}
public Order build() {
validate(); // 校验必填字段
return new Order(this);
}
private void validate() {
if (orderId == null || userId == null || amount == null) {
throw new IllegalArgumentException("必填字段缺失");
}
}
}
}
// 使用示例:组合优惠订单
Order order = new Order.OrderBuilder("20230901001", "user123", new BigDecimal("299.00"))
.withCoupon("DISCOUNT_20") // 添加折扣券
.addGift("保温杯") // 添加赠品
.build();
2. 游戏角色创建(可视化配置)
痛点:发型、肤色、装备等20+种可定制属性
java
// 产品类:游戏角色
public class GameCharacter {
private String name; // 角色名
private String hairStyle; // 发型(默认:短发)
private String armor; // 护甲(默认:布衣)
private String weapon; // 武器(默认:木剑)
// 建造者(与产品强关联)
public static class Builder {
private final GameCharacter character = new GameCharacter();
public Builder(String name) { // 必须指定角色名
character.name = name;
character.hairStyle = "短发"; // 默认值
character.armor = "布衣";
character.weapon = "木剑";
}
public Builder hairStyle(String style) {
character.hairStyle = style;
return this;
}
public Builder armor(String armor) {
character.armor = armor;
return this;
}
public Builder weapon(String weapon) {
character.weapon = weapon;
return this;
}
public GameCharacter build() {
return character; // 直接返回已配置对象
}
}
}
// 使用示例:创建豪华装备角色
GameCharacter warrior = new GameCharacter.Builder("战神")
.hairStyle("金色长发")
.armor("龙鳞铠甲")
.weapon("屠龙宝刀")
.build();
3. 报表生成器(分步配置)
痛点:表头、数据源、样式配置的复杂组合
java
// 产品类:Excel报表
public class ExcelReport {
private List<String> headers;
private List<List<Object>> data;
private String style;
private boolean autoSize;
private ExcelReport() {} // 强制使用建造者
// 建造者
public static class ReportBuilder {
private final ExcelReport report = new ExcelReport();
public ReportBuilder setHeaders(String... headers) {
report.headers = Arrays.asList(headers);
return this;
}
public ReportBuilder addDataRow(List<Object> row) {
if (report.data == null) {
report.data = new ArrayList<>();
}
report.data.add(row);
return this;
}
public ReportBuilder useModernStyle() {
report.style = "现代风格";
report.autoSize = true;
return this;
}
public ExcelReport build() {
if (report.headers == null || report.data == null) {
throw new IllegalStateException("缺少表头或数据");
}
return report;
}
}
}
// 使用示例:销售报表
ExcelReport report = new ExcelReport.ReportBuilder()
.setHeaders("日期", "产品", "销售额")
.addDataRow(Arrays.asList("2023-09-01", "手机", 500000))
.addDataRow(Arrays.asList("2023-09-01", "笔记本", 300000))
.useModernStyle()
.build();
4. HTTP请求构造(不可变对象)
痛点:Header、Body、超时时间等参数的灵活组合
java
// 产品类:HTTP请求(不可变)
public final class HttpRequest {
private final String method;
private final String url;
private final Map<String, String> headers;
private final String body;
private final int timeout;
// 私有构造器
private HttpRequest(Builder builder) {
this.method = builder.method;
this.url = builder.url;
this.headers = Collections.unmodifiableMap(builder.headers);
this.body = builder.body;
this.timeout = builder.timeout;
}
// 建造者
public static class Builder {
private String method = "GET"; // 默认值
private String url;
private Map<String, String> headers = new HashMap<>();
private String body;
private int timeout = 5000; // 默认5秒
public Builder(String url) { // 必须参数
this.url = url;
}
public Builder method(String method) {
this.method = method.toUpperCase();
return this;
}
public Builder addHeader(String key, String value) {
headers.put(key, value);
return this;
}
public Builder body(String body) {
this.body = body;
return this;
}
public Builder timeout(int timeout) {
this.timeout = timeout;
return this;
}
public HttpRequest build() {
if (url == null) throw new IllegalArgumentException("URL不能为空");
if ("POST".equals(method) && body == null) {
throw new IllegalStateException("POST请求必须包含Body");
}
return new HttpRequest(this);
}
}
}
// 使用示例:构造POST请求
HttpRequest request = new HttpRequest.Builder("https://api.example.com/data")
.method("POST")
.addHeader("Content-Type", "application/json")
.body("{\"key\":\"value\"}")
.timeout(10000)
.build();
场景共性总结
场景特点 | 建造者模式的解决方案 |
---|---|
参数组合多变 | 通过链式调用自由组合 |
必填/选填参数混杂 | 强制在构造器中设置必填项 |
需要参数校验 | 在build()方法集中校验逻辑 |
创建过程需要明确语义 | 方法命名体现参数含义(如withXxx) |
需要构建不可变对象 | 通过final属性和unmodifiable集合实现 |
选择建议:当遇到"参数超过4个且存在多种组合方式"的情况,就该考虑建造者模式了!
四、手把手代码实现(含完整Java代码)
案例:电商平台优惠券系统
场景需求 :
设计一个支持多种优惠券组合的订单系统,要求:
- 必须包含订单ID、用户ID、总金额
- 可选添加优惠券、赠品、备注
- 金额必须大于0,优惠券需校验有效期
- 保证订单对象的不可变性
第一步:定义产品类(不可变订单)
java
import java.math.BigDecimal;
import java.time.LocalDate;
// 不可变订单类
public final class CouponOrder {
// 必填字段(final修饰)
private final String orderId;
private final String userId;
private final BigDecimal totalAmount;
// 可选字段(final修饰)
private final String couponCode;
private final String gift;
private final String remark;
// 私有构造器(只能通过Builder创建)
private CouponOrder(Builder builder) {
this.orderId = builder.orderId;
this.userId = builder.userId;
this.totalAmount = builder.totalAmount;
this.couponCode = builder.couponCode;
this.gift = builder.gift;
this.remark = builder.remark;
}
// Getter方法(无Setter)
public String getOrderId() { return orderId; }
public String getUserId() { return userId; }
public BigDecimal getTotalAmount() { return totalAmount; }
public String getCouponCode() { return couponCode; }
public String getGift() { return gift; }
public String getRemark() { return remark; }
}
第二步:实现建造者(内部类方式)
java
public final class CouponOrder {
// ... 其他代码同上 ...
// 建造者(静态内部类)
public static class Builder {
// 必填参数(通过构造函数强制要求)
private final String orderId;
private final String userId;
private final BigDecimal totalAmount;
// 可选参数(提供默认值)
private String couponCode = "NO_COUPON";
private String gift = "";
private String remark = "";
private LocalDate couponExpireDate;
// 强制必填参数
public Builder(String orderId, String userId, BigDecimal totalAmount) {
this.orderId = orderId;
this.userId = userId;
this.totalAmount = totalAmount;
}
// 可选参数配置方法(返回this实现链式调用)
public Builder withCoupon(String code, LocalDate expireDate) {
this.couponCode = code;
this.couponExpireDate = expireDate;
return this;
}
public Builder addGift(String gift) {
this.gift = gift;
return this;
}
public Builder addRemark(String remark) {
this.remark = remark;
return this;
}
public CouponOrder build() {
validate();
return new CouponOrder(this);
}
// 参数校验逻辑
private void validate() {
if (orderId == null || orderId.isEmpty()) {
throw new IllegalArgumentException("订单ID不能为空");
}
if (totalAmount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("订单金额必须大于0");
}
if (couponExpireDate != null && couponExpireDate.isBefore(LocalDate.now())) {
throw new IllegalArgumentException("优惠券已过期");
}
}
}
}
第三步:使用示例
java
public class Main {
public static void main(String[] args) {
// 基础订单
CouponOrder order1 = new CouponOrder.Builder("20230901001", "user123", new BigDecimal("299.00"))
.build();
// 含优惠券和赠品的订单
CouponOrder order2 = new CouponOrder.Builder("20230901002", "user456", new BigDecimal("599.00"))
.withCoupon("SUMMER-50", LocalDate.of(2023, 9, 30))
.addGift("无线鼠标")
.addRemark("请周末配送")
.build();
// 错误示例:金额不合法
try {
CouponOrder invalidOrder = new CouponOrder.Builder("20230901003", "user789", new BigDecimal("-100"))
.build();
} catch (IllegalArgumentException e) {
System.out.println("错误捕获: " + e.getMessage());
}
}
}
代码设计亮点
设计点 | 实现方式 | 解决的问题 |
---|---|---|
强制必填参数 | 通过Builder构造函数强制要求 | 避免必填项缺失 |
防御性拷贝 | 使用BigDecimal不可变类 | 防止金额被外部修改 |
链式调用 | 每个配置方法返回this | 提升代码可读性 |
过期校验 | 在build()时检查优惠券有效期 | 业务规则前置校验 |
不可变对象 | final修饰字段 + 无setter方法 | 线程安全 + 数据一致性 |
常见问题解决示例
问题:如何实现"满100减20"的优惠券?
java
public Builder applyDiscount(BigDecimal threshold, BigDecimal discount) {
if (totalAmount.compareTo(threshold) >= 0) {
this.totalAmount = this.totalAmount.subtract(discount); // 错误!BigDecimal不可变
}
return this;
}
正确实现方式:
java
public Builder applyDiscount(BigDecimal threshold, BigDecimal discount) {
if (totalAmount.compareTo(threshold) >= 0) {
// 创建新的Builder实例(需要重新设计Builder结构)
return new Builder(orderId, userId, totalAmount.subtract(discount))
.withCoupon(couponCode, couponExpireDate)
.addGift(gift)
.addRemark(remark);
}
return this;
}
关键点:当需要修改不可变字段时,必须创建新的Builder实例
扩展:Lombok简化版
java
import lombok.Builder;
import lombok.NonNull;
@Builder
public class LombokOrder {
@NonNull private final String orderId;
@NonNull private final String userId;
@NonNull private final BigDecimal totalAmount;
@Builder.Default private String couponCode = "NO_COUPON";
@Builder.Default private String gift = "";
private LocalDate couponExpireDate;
}
// 使用方式
LombokOrder order = LombokOrder.builder()
.orderId("20230901004")
.userId("user100")
.totalAmount(new BigDecimal("199.00"))
.couponCode("WELCOME-10")
.build();
注意:Lombok版本需自行添加参数校验逻辑
总结
通过这个电商优惠券系统的案例,我们实现了:
- 通过建造者模式优雅处理6个以上参数
- 使用不可变对象保证线程安全
- 在build()时集中校验业务规则
- 通过链式调用提升代码可读性
实际项目建议:当你的对象满足以下条件时使用建造者模式:
- 参数数量 ≥ 4个
- 存在必填/选填参数组合
- 需要创建不可变对象
- 存在复杂的参数校验逻辑
五、高级技巧:Lombok实现建造者模式
java
@Builder
public class Computer {
private String cpu;
private String gpu;
private int ram;
}
// 使用方式
Computer pc = Computer.builder()
.cpu("Ryzen 7")
.gpu("RX 7900")
.ram(32)
.build();
六、项目实战建议
-
何时使用
- 当对象需要多个可变参数时
- 需要创建不同表示的对象时(如不同套餐配置)
-
常见坑点
- 忘记调用build()方法
- 未做参数校验导致非法状态
-
性能优化:重用Builder实例的场景
七、常见问题解答
-
Q:建造者模式和工厂模式有什么区别?
A:工厂关注产品类型,建造者关注构建过程
-
Q:是否每个类都需要Builder?
A:参数小于4个时可能过度设计
-
Q:如何保证线程安全?
A:为每个线程创建独立Builder实例