设计模式--建造者模式 (Builder)
建造者模式将复杂对象的构建与其表示分离,用于分步创建一个复杂对象。它解决了构造函数参数过多的问题,并能使用同一构建过程创建不同表示的对象。
我们来详细地解析一下设计模式中的建造者模式(Builder Pattern)。
我会从以下几个方面来为你讲解:
- 什么是建造者模式?(定义和比喻)
- 为什么要使用建造者模式?(解决的问题)
- 建造者模式的结构和角色
- 代码实现(两种常见形式)
- 经典形式(带指导者 Director)
- 链式调用形式(更常用)
- 优缺点分析
- 适用场景
- 与工厂模式的区别
1. 什么是建造者模式?
核心思想: 将一个复杂对象的构建过程与其表示分离,使得同样的构建过程可以创建出不同的表示。
简单来说,建造者模式关心的是如何一步一步地创建一个复杂对象,它允许你使用相同的构建代码生成不同类型和表示的对象。
一个生动的比喻:
想象一下去快餐店点一个自定义套餐(比如赛百味或组装电脑)。
- 产品(Product):你最终得到的那个汉堡或电脑。它很复杂,由多个部分组成(面包、肉、蔬菜、酱料等)。
- 抽象建造者(Builder):一个标准的“配餐单”,上面列出了所有可添加的步骤,比如
addBread(),addMeat(),addVegetable()。但它不规定具体加哪种面包或肉。 - 具体建造者(Concrete Builder):厨师 A 和厨师 B。他们都拿着同样的配餐单。
- 厨师 A(
豪华牛肉套餐建造者)按照配餐单,加入了全麦面包、双层牛肉、生菜和番茄酱。 - 厨师 B(
健康鸡肉套餐建造者)按照配餐单,加入了白面包、烤鸡肉、所有蔬菜和零卡酱。
- 厨师 A(
- 指导者(Director):你自己(或者一个固定的套餐流程)。你告诉厨师:“先加面包,再加肉,最后加蔬菜和酱料”。这个顺序和流程是固定的。你不需要关心厨师具体是怎么做牛肉的,你只负责指挥构建的步骤。
通过这个过程,你(指导者)使用同样的指令(构建过程),让不同的厨师(具体建造者)做出了完全不同风味的汉堡(不同的表示)。
2. 为什么要使用建造者模式?
主要为了解决以下两个问题:
构造函数参数过多(Telescoping Constructor Problem)
当一个类有很多属性,其中一些是必需的,一些是可选的,你可能会创建很多个构造函数:java// 糟糕的设计:伸缩构造函数 public class Computer { // 必选 private String cpu; private int ram; // 可选 private int ssd; private String gpu; private String keyboard; public Computer(String cpu, int ram) { ... } public Computer(String cpu, int ram, int ssd) { ... } public Computer(String cpu, int ram, int ssd, String gpu) { ... } // ... 还有很多组合 } // 调用时非常痛苦 Computer pc = new Computer("Intel i7", 16, 512, "RTX 3080", null);这样的代码可读性差,容易传错参数,且无法清晰地表达每个参数的含义。
对象状态不一致
另一种方法是使用无参构造函数和 setter 方法:javaComputer pc = new Computer(); pc.setCpu("Intel i7"); pc.setRam(16); // ... // 在调用完所有 setter 之前,pc 对象可能处于一个不完整或无效的状态这种方式下,对象在完全构建好之前,可能被其他代码使用,导致不可预期的错误。
建造者模式通过将构建过程封装起来,完美地解决了这两个问题。
3. 建造者模式的结构和角色
- Product(产品):要创建的复杂对象。它包含了多个组成部分。
- Builder(抽象建造者):一个接口,定义了创建产品各个部分的方法(如
buildPartA(),buildPartB())以及一个返回最终产品的方法getResult()。 - ConcreteBuilder(具体建造者):实现了 Builder 接口。它知道如何构建和装配产品的各个部分,并负责维护其正在创建的产品实例。
- Director(指导者):负责管理构建过程。它持有一个 Builder 对象的引用,并调用 Builder 的方法来构建产品。Director 定义了构建的顺序和算法,将客户端与具体的构建过程解耦。(注意:在现代的链式调用实现中,这个角色常常被省略)。
4. 代码实现
形式一:经典形式(带 Director)
这种形式最符合 GoF 的原始定义,强调“构建过程”的稳定。
场景: 组装电脑,分为游戏电脑和办公电脑。
1. 产品 (Product)
// Computer.java
public class Computer {
private String cpu;
private String ram;
private String ssd;
private String gpu;
// Setter methods
public void setCpu(String cpu) { this.cpu = cpu; }
public void setRam(String ram) { this.ram = ram; }
public void setSsd(String ssd) { this.ssd = ssd; }
public void setGpu(String gpu) { this.gpu = gpu; }
@Override
public String toString() {
return "Computer [cpu=" + cpu + ", ram=" + ram + ", ssd=" + ssd + ", gpu=" + gpu + "]";
}
}
2. 抽象建造者 (Builder)
// ComputerBuilder.java
public interface ComputerBuilder {
void buildCpu();
void buildRam();
void buildSsd();
void buildGpu();
Computer getComputer();
}
3. 具体建造者 (ConcreteBuilder)
// GamingComputerBuilder.java
public class GamingComputerBuilder implements ComputerBuilder {
private Computer computer = new Computer();
@Override
public void buildCpu() {
computer.setCpu("Intel i9");
}
@Override
public void buildRam() {
computer.setRam("32GB DDR5");
}
@Override
public void buildSsd() {
computer.setSsd("1TB NVMe SSD");
}
@Override
public void buildGpu() {
computer.setGpu("NVIDIA RTX 4090");
}
@Override
public Computer getComputer() {
return computer;
}
}
// OfficeComputerBuilder.java
public class OfficeComputerBuilder implements ComputerBuilder {
private Computer computer = new Computer();
@Override
public void buildCpu() {
computer.setCpu("Intel i5");
}
@Override
public void buildRam() {
computer.setRam("16GB DDR4");
}
@Override
public void buildSsd() {
computer.setSsd("512GB SATA SSD");
}
@Override
public void buildGpu() {
// 办公电脑通常使用集成显卡
computer.setGpu("Integrated Graphics");
}
@Override
public Computer getComputer() {
return computer;
}
}
4. 指导者 (Director)
// ComputerDirector.java
public class ComputerDirector {
// 指挥构建过程
public Computer construct(ComputerBuilder builder) {
builder.buildCpu();
builder.buildRam();
builder.buildSsd();
builder.buildGpu();
return builder.getComputer();
}
}
5. 客户端调用
public class Client {
public static void main(String[] args) {
ComputerDirector director = new ComputerDirector();
// 构建一台游戏电脑
ComputerBuilder gamingBuilder = new GamingComputerBuilder();
Computer gamingComputer = director.construct(gamingBuilder);
System.out.println("Gaming Computer: " + gamingComputer);
// 构建一台办公电脑
ComputerBuilder officeBuilder = new OfficeComputerBuilder();
Computer officeComputer = director.construct(officeBuilder);
System.out.println("Office Computer: " + officeComputer);
}
}
形式二:链式调用(Fluent Interface)
这是目前更流行、更实用的形式,常见于各种框架和库中(如 StringBuilder, OkHttp 的 Request.Builder, Lombok 的 @Builder 注解)。它通常省略了 Director 角色,将构建的灵活性直接交给客户端。
1. 产品 (Product) - 稍作修改
// Computer.java
public class Computer {
private final String cpu; // 使用 final 确保不可变
private final String ram;
private final String ssd;
private final String gpu;
// 构造函数私有,只能通过 Builder 创建
private Computer(Builder builder) {
this.cpu = builder.cpu;
this.ram = builder.ram;
this.ssd = builder.ssd;
this.gpu = builder.gpu;
}
@Override
public String toString() {
return "Computer [cpu=" + cpu + ", ram=" + ram + ", ssd=" + ssd + ", gpu=" + gpu + "]";
}
// 静态内部类 Builder
public static class Builder {
// 必选参数
private final String cpu;
private final String ram;
// 可选参数
private String ssd = "256GB"; // 可以有默认值
private String gpu = "Integrated";
public Builder(String cpu, String ram) {
this.cpu = cpu;
this.ram = ram;
}
public Builder ssd(String ssd) {
this.ssd = ssd;
return this; // 返回 this 以支持链式调用
}
public Builder gpu(String gpu) {
this.gpu = gpu;
return this; // 返回 this
}
// 最终的 build 方法
public Computer build() {
// 可以在这里进行参数校验
return new Computer(this);
}
}
}
2. 客户端调用
public class Client {
public static void main(String[] args) {
// 使用链式调用构建对象
Computer gamingComputer = new Computer.Builder("Intel i9", "32GB")
.ssd("2TB NVMe SSD")
.gpu("NVIDIA RTX 4090")
.build();
Computer officeComputer = new Computer.Builder("Intel i5", "16GB")
.ssd("512GB SATA SSD")
// 不设置 gpu,使用默认值
.build();
System.out.println("Gaming Computer: " + gamingComputer);
System.out.println("Office Computer: " + officeComputer);
}
}
这种形式可读性极强,并且通过将构造函数私有化和在 build() 方法中返回最终对象,保证了对象的不可变性和状态一致性。
5. 优缺点分析
优点:
- 封装性好:将构建细节封装在 ConcreteBuilder 中,客户端无需知道内部的构建逻辑。
- 易于扩展:添加一个新的产品表示,只需要增加一个新的 ConcreteBuilder 类即可,符合“开闭原则”。
- 更好的控制:
- 经典模式中,Director 控制了构建的顺序和过程,确保了构建的稳定性。
- 链式模式中,客户端可以自由地、清晰地配置对象的属性,可读性高。
- 将构建和表示分离:使得相同的构建过程可以创建出不同的产品,复用性好。
缺点:
- 增加了代码量:引入了 Builder、Director 等角色,使得类的数量增多,系统结构更复杂。
- 不适用于简单对象:如果一个对象不够复杂,属性不多,使用建造者模式会显得“杀鸡用牛刀”,增加不必要的复杂性。
6. 适用场景
- 当一个对象的创建过程很复杂,由多个部分组成,并且这些部分的创建顺序会影响最终结果时。
- 当需要创建的对象具有很多可选参数或配置时(完美替代伸缩构造函数)。
- 当希望隔离复杂对象的创建和使用,并让客户端能够自定义对象的具体构成时。
- 当需要创建同一类的不同变体(表示)时,例如,创建一套“豪华版”和“标准版”的配置。
7. 与工厂模式的区别
这是面试中经常被问到的问题,两者都用于创建对象,但侧重点完全不同。
| 特性 | 建造者模式 (Builder) | 工厂模式 (Factory) |
|---|---|---|
| 关注点 | 过程:关注如何一步一步地创建出一个复杂对象。它是一个“施工队”。 | 结果:关注如何直接创建出一个完整的对象实例。它是一个“生产线”。 |
| 对象复杂度 | 主要用于复杂对象的创建。 | 可用于简单对象,也可用于复杂对象。 |
| 创建方式 | 客户端(或 Director)参与了对象的构建过程,可以分步进行。 | 客户端直接向工厂请求一个对象,不关心创建细节。 |
| 产品差异性 | 产出的产品通常是同一类型但配置不同的对象。 | 抽象工厂模式产出的是一族相关但不同类型的对象(如WindowsButton和MacButton)。 |
| 抽象级别 | 更高的抽象层次,将构建算法和具体实现分离。 | 抽象层次相对较低,主要隐藏了 new 的过程。 |
简单总结: 如果你想知道一个对象是如何被组装起来的,并且想控制这个组装过程,用建造者模式。如果你只关心得到一个什么类型的对象,不关心它是怎么来的,用工厂模式。