8. 建造者模式:复杂对象的构建、Builder模式的定义、链式调用实现
说实话,我第一次接触建造者模式,是在做一个电商订单系统的时候。那个订单对象,参数多得吓人——收货地址、商品列表、优惠券、支付方式、发票信息……十几个字段,有的必填,有的选填,有的还有依赖关系。用构造方法?那得写多少个重载啊。用setter?调用方得写一堆啰嗦的代码,还容易漏掉必填项。
嗯,当时我就想,肯定有更好的办法。后来翻到《Effective Java》里讲Builder模式的那一章,豁然开朗。说白了,建造者模式就是专门用来对付这种“构造参数太多、太复杂”的场景的。
8.1 什么是建造者模式?
建造者模式(Builder Pattern)的核心思想很简单:将一个复杂对象的构建过程,与它的表示分离开来。这样,同样的构建过程,可以创建出不同的表示。
你想想看,我们平时点外卖,选套餐的过程就是典型的建造者模式。你告诉店员:“我要一个汉堡,加一份薯条,饮料要可乐。”店员按照你的要求,一步步把套餐组装好。至于这个套餐最后是用纸袋装还是托盘装,那是表示层的事,跟构建过程无关。
核心定义:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
8.2 为什么需要建造者模式?
我总结了一下,主要有这么几个痛点,建造者模式都能解决:
- 参数太多:一个类有5个以上构造参数,调用方很容易搞混顺序。
- 参数有依赖:比如选了“加冰”就必须选“冷饮”,这种校验逻辑散落在各处。
- 对象不可变:我们希望对象创建后就不能修改,但用setter又破坏了不可变性。
- 构建过程复杂:需要分步骤、按顺序组装,每一步都可能出错。
我记得有一次,团队里有个同事写了一个类,构造方法有8个参数,全是String类型。调用的时候,传参顺序错了,结果线上出了个隐蔽的bug。排查了一下午才发现,是“手机号”和“身份证号”传反了。从那以后,我就在团队里强制推行了建造者模式。
8.3 建造者模式的经典结构
建造者模式通常包含这几个角色:
| 角色 | 职责 | 类比 |
|---|---|---|
| Product(产品) | 最终要构建的复杂对象 | 套餐 |
| Builder(抽象建造者) | 定义构建步骤的接口 | 套餐制作流程 |
| ConcreteBuilder(具体建造者) | 实现构建步骤,并返回产品 | 店员A(按流程做) |
| Director(指挥者) | 控制构建的顺序和逻辑 | 店长(指挥怎么做) |
下面这张图,把建造者模式的整体流程画出来了:
8.4 链式调用的实现
在实际开发中,我们最常用的其实是链式调用的变体。说白了,就是把Builder的方法返回this,这样就可以连续调用了。Java里最经典的例子就是StringBuilder和StringBuffer。
下面我写一个实际的例子。假设我们要构建一个Computer对象,它有CPU、内存、硬盘、显卡等配置,有些是必填的,有些是选填的。
public class Computer {
// 必填参数
private final String cpu;
private final String memory;
// 选填参数
private final String hardDisk;
private final String graphicsCard;
private final String powerSupply;
// 私有构造方法,只能通过Builder创建
private Computer(Builder builder) {
this.cpu = builder.cpu;
this.memory = builder.memory;
this.hardDisk = builder.hardDisk;
this.graphicsCard = builder.graphicsCard;
this.powerSupply = builder.powerSupply;
}
// 静态内部类 Builder
public static class Builder {
// 必填参数
private final String cpu;
private final String memory;
// 选填参数,给默认值
private String hardDisk = "机械硬盘 1TB";
private String graphicsCard = "集成显卡";
private String powerSupply = "500W";
// Builder的构造方法,只接收必填参数
public Builder(String cpu, String memory) {
this.cpu = cpu;
this.memory = memory;
}
// 链式调用方法,返回this
public Builder hardDisk(String hardDisk) {
this.hardDisk = hardDisk;
return this;
}
public Builder graphicsCard(String graphicsCard) {
this.graphicsCard = graphicsCard;
return this;
}
public Builder powerSupply(String powerSupply) {
this.powerSupply = powerSupply;
return this;
}
// 最后调用build()方法,创建Computer对象
public Computer build() {
// 可以在这里做参数校验
if (cpu == null || memory == null) {
throw new IllegalStateException("CPU和内存不能为空");
}
return new Computer(this);
}
}
// getter方法,没有setter,保证不可变性
public String getCpu() { return cpu; }
public String getMemory() { return memory; }
public String getHardDisk() { return hardDisk; }
public String getGraphicsCard() { return graphicsCard; }
public String getPowerSupply() { return powerSupply; }
}
调用的时候,就非常清爽了:
Computer computer = new Computer.Builder("i7-13700K", "32GB DDR5")
.hardDisk("三星 990 Pro 2TB")
.graphicsCard("RTX 4080")
.powerSupply("850W 金牌")
.build();
System.out.println(computer.getCpu()); // i7-13700K
System.out.println(computer.getGraphicsCard());// RTX 4080
个人习惯:我一般把Builder作为静态内部类放在Product里面。这样调用方一眼就能看到“哦,这个类是用Builder创建的”,而且Builder可以直接访问Product的私有成员,代码更简洁。
8.5 避坑指南
我曾经踩过一个坑,在这里分享给大家:
坑1:Builder里不要做太复杂的业务逻辑。 Builder的职责就是构建对象,校验参数可以,但别在里面查数据库、调远程服务。否则你调用build()的时候,可能卡住好几秒,调用方会一脸懵。
坑2:注意线程安全。 Builder本身不是线程安全的。如果你在多线程环境下构建对象,记得加锁,或者每次new一个新的Builder。我个人习惯是每次构建都new,反正Builder对象很轻量。
坑3:不要滥用建造者模式。 如果一个类只有两三个参数,用构造方法或者setter就够了。强行用Builder反而显得过度设计。我一般遵循一个原则:参数超过4个,或者参数之间有复杂的依赖关系,才考虑用建造者模式。
8.6 建造者模式 vs 工厂模式
很多初学者会把这两个模式搞混。我简单说一下区别:
- 工厂模式:关注的是“创建哪个产品”,比如根据参数决定返回A还是B。
- 建造者模式:关注的是“怎么一步步创建产品”,比如先装CPU再装内存。
说白了,工厂模式是一步到位,建造者模式是分步组装。如果产品构造很简单,用工厂模式就够了。如果产品构造很复杂,需要分步骤、有顺序、有校验,那就用建造者模式。
注意:建造者模式会增加类的数量。每个产品类都要配一个Builder类,代码量会翻倍。所以不要为了用模式而用模式,要真正有复杂构建的需求时再上。
8.7 实际项目中的应用
我记得在做一个支付网关的时候,需要构建一个支付请求对象。这个对象有几十个字段,包括商户号、订单号、金额、回调地址、商品描述、附加参数……而且不同支付渠道(微信、支付宝、银联)要求的字段还不一样。
用建造者模式之后,代码变得非常清晰:
PaymentRequest request = new PaymentRequest.Builder("MERCHANT_001", "ORDER_2024001", new BigDecimal("99.99"))
.channel(PaymentChannel.WECHAT)
.notifyUrl("https://api.myshop.com/pay/notify")
.description("购买《Java设计模式精讲》课程")
.extraParam("openId", "oABC123xyz")
.build();
你看,调用方只需要关心他要配置哪些参数,完全不用管内部怎么组装。而且因为Builder方法返回的是this,IDE的自动补全用起来特别顺手,基本不会漏掉必填项。
嗯,这就是建造者模式的核心价值——让复杂对象的构建变得简单、清晰、可靠。
公众号:蓝海资料掘金营,微信 deep3321