建造者模式:把复杂对象的组装过程封装起来
说实话,建造者模式是我在实际项目中用得最多的创建型模式之一。为什么?因为我们在真实开发中,经常会遇到那种「参数多到令人发指」的对象。你想想看,一个订单对象可能有十几个字段,有些必填、有些可选、有些还有依赖关系——这时候如果直接用构造函数,那代码简直没法看。
建造者模式的核心思想,说白了就是:把对象的构建过程和它的最终表示分离开。同样的构建步骤,可以产出不同的产品形态。就像做汉堡,步骤永远是「选面包→加肉饼→加蔬菜→加酱料」,但你可以做出牛肉汉堡、鸡肉汉堡、素汉堡——流程一样,结果不同。
什么时候该用建造者模式?
我个人习惯,当出现以下信号时,就会考虑引入建造者模式:
- 对象的构造函数参数超过4个,而且很多是可选参数
- 参数之间存在依赖关系(比如A参数必须和B参数一起出现)
- 对象的构建过程有固定的步骤顺序
- 同样的构建过程需要产出不同的产品变体
我在项目中遇到过这样一个场景:一个报表配置对象,有数据源、过滤条件、排序规则、分页设置、图表类型、颜色主题、导出格式……总共十几个参数。最开始大家用重载构造函数,后来改用setter方法,但总有人忘记调用某个必要的setter。最后我们用建造者模式重构,代码清晰多了,bug也少了一大半。
建造者模式的结构
先来看一下建造者模式的经典结构。我画了一张图,帮你快速理解各个角色的关系:
这张图里,Director 负责控制构建流程,它不关心具体产品长什么样。Builder 是抽象接口,定义了构建步骤。ConcreteBuilder 实现具体的构建逻辑,最终产出 Product。
代码示例:从糟糕到优雅
先看一个反面教材。假设我们要构建一个电脑配置对象:
// 糟糕的做法: telescoping constructor 模式
public class Computer {
private String cpu;
private String gpu;
private int ram;
private int storage;
private boolean hasBluetooth;
private boolean hasWifi;
public Computer(String cpu, String gpu, int ram, int storage) {
this(cpu, gpu, ram, storage, false, false);
}
public Computer(String cpu, String gpu, int ram, int storage, boolean hasBluetooth) {
this(cpu, gpu, ram, storage, hasBluetooth, false);
}
public Computer(String cpu, String gpu, int ram, int storage,
boolean hasBluetooth, boolean hasWifi) {
this.cpu = cpu;
this.gpu = gpu;
this.ram = ram;
this.storage = storage;
this.hasBluetooth = hasBluetooth;
this.hasWifi = hasWifi;
}
}
// 调用时,你根本分不清参数的含义
Computer c = new Computer("i7", "RTX3060", 16, 512, true, false);
这种代码,我每次看到都想叹气。参数一多,调用方根本记不住顺序,而且可读性极差。用建造者模式重构一下:
public class Computer {
// 所有字段都是 final,保证不可变性
private final String cpu;
private final String gpu;
private final int ram;
private final int storage;
private final boolean hasBluetooth;
private final boolean hasWifi;
// 私有构造函数,只能通过 Builder 创建
private Computer(Builder builder) {
this.cpu = builder.cpu;
this.gpu = builder.gpu;
this.ram = builder.ram;
this.storage = builder.storage;
this.hasBluetooth = builder.hasBluetooth;
this.hasWifi = builder.hasWifi;
}
// 静态内部 Builder 类
public static class Builder {
// 必填参数
private final String cpu;
private final int ram;
// 可选参数,设置默认值
private String gpu = "集成显卡";
private int storage = 256;
private boolean hasBluetooth = false;
private boolean hasWifi = true;
public Builder(String cpu, int ram) {
this.cpu = cpu;
this.ram = ram;
}
public Builder gpu(String gpu) {
this.gpu = gpu;
return this;
}
public Builder storage(int storage) {
this.storage = storage;
return this;
}
public Builder bluetooth(boolean hasBluetooth) {
this.hasBluetooth = hasBluetooth;
return this;
}
public Builder wifi(boolean hasWifi) {
this.hasWifi = hasWifi;
return this;
}
public Computer build() {
// 可以在这里做参数校验
if (ram < 4) {
throw new IllegalArgumentException("内存至少4GB");
}
return new Computer(this);
}
}
}
// 调用方式:链式调用,清晰明了
Computer computer = new Computer.Builder("i7", 16)
.gpu("RTX3060")
.storage(512)
.bluetooth(true)
.build();
关键点:建造者模式让对象的构建过程变得「可读」且「可控」。每个setter方法返回Builder自身,实现链式调用。最终通过build()方法产出不可变的对象。
避坑指南
我曾经踩过一个坑:在一个项目中,我们给Builder的每个setter方法都做了复杂的参数校验。结果build()方法反而没做校验。后来发现,有些调用方跳过了某些setter,直接调build(),导致生成了不合法的对象。
我的建议是:参数校验集中在build()方法里做。setter方法只做简单的类型转换和赋值,不要在里面写复杂的业务逻辑。这样逻辑更集中,也更容易维护。
建造者模式的变体
在实际开发中,建造者模式有几种常见的变体,我简单列一下:
| 变体名称 | 适用场景 | 特点 |
|---|---|---|
| 经典建造者 | 有固定的构建步骤,需要不同的产品表示 | 有Director角色,控制构建流程 |
| 简化建造者 | 没有复杂的步骤顺序,只是参数多 | 去掉Director,Builder直接返回产品 |
| 递归建造者 | 对象包含子对象,需要嵌套构建 | Builder支持泛型继承,实现链式调用 |
我个人最常用的是简化建造者,也就是上面代码示例那种。大多数业务场景其实不需要Director,因为构建步骤没那么复杂。Director更适合那种「构建流程本身会变化」的场景,比如不同版本的报表生成器。
与工厂模式的区别
很多初学者会把建造者模式和工厂模式搞混。我简单说一下我的理解:
- 工厂模式:关注的是「创建哪个产品」,把选择权交给工厂,调用方不关心具体类名
- 建造者模式:关注的是「怎么创建产品」,把组装过程拆解成步骤,调用方可以精细控制
说白了,工厂模式是「我给你参数,你给我产品」。建造者模式是「我一步步告诉你我要什么,最后你组装好给我」。两者不冲突,甚至可以结合使用——工厂负责选择具体的建造者,建造者负责组装产品。
小技巧:如果你发现一个类的构造函数参数超过5个,或者有超过3个可选参数,那就应该考虑用建造者模式了。别等到代码写成一团乱麻再重构,那时候改起来成本高得多。
实际项目中的应用
我记得在一个微服务项目中,我们需要构建复杂的查询条件对象。这个对象有分页、排序、过滤、字段选择、聚合计算等等。最开始用Map传参,代码里到处都是字符串常量,维护起来简直噩梦。
后来我们用建造者模式重构了查询构建器:
Query query = new Query.Builder("users")
.select("id", "name", "email")
.where("age", ">", 18)
.and("status", "=", "active")
.orderBy("createTime", SortOrder.DESC)
.page(1, 20)
.build();
你看,代码读起来就像在写SQL语句一样自然。这就是建造者模式的魅力——它让代码的调用方式接近自然语言,可读性极强。
嗯,这里要注意一点:建造者模式虽然好用,但也不是万能的。如果你的对象只有两三个参数,用建造者模式反而显得过度设计。记住,模式是为了解决问题而存在的,不是为了炫技。
公众号:蓝海资料掘金营,微信deep3321