九、建造者模式(Builder)
1、介绍
建造者模式(Builder)是一种创建型设计模式,它主要是用来解决在软件系统中,对于一些复杂对象的构建和表示。它将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。
在很多软件系统中,我们经常会遇到这样的情况:一个对象由多个部分组成,并且对象的创建过程需要遵循特定的步骤。例如,一份完整的电脑配置可能包括处理器、内存、硬盘、显示器等组件,这些组件需要按照特定的顺序和配置进行组装。在这种情况下,如果我们将创建过程直接编码在对象中,那么会导致代码冗余,扩展性差,且容易引发错误。
为了解决这个问题,我们可以使用建造者模式。在建造者模式中,我们会创建一个 Builder 类,这个类定义了所有必要的步骤和序列来创建对象。然后,我们可以创建一个 Director 类,它接受一个 Builder 对象,并通过调用 Builder 对象的方法来创建对象。最后,我们可以创建多个不同的 Builder 对象,这样就可以创建出具有不同表示的对象。
总的来说,建造者模式能够让我们更灵活地创建复杂对象,提高代码的可读性和可维护性,同时也降低了错误的可能性。
2、生活实例
想象你要在快餐店里点一份套餐。你可以选择汉堡的种类,是否要奶酪,是否要薯条,是否要加大,饮料是可乐还是果汁等等。在快餐店里,服务员会根据你的需求来制作套餐,这就类似于建造者模式。在建造者模式中,你不直接去制作一个对象,而是给出你的需求,然后有一个“建造者”根据你的需求来制作对象。
3、java代码实例
在没有使用建造者模式的情况下,我们可能会看到像这样的代码:
public class Computer {
private String CPU;
private String memory;
private String hardDrive;
private String monitor;
public Computer(String CPU, String memory, String hardDrive, String monitor) {
this.CPU = CPU;
this.memory = memory;
this.hardDrive = hardDrive;
this.monitor = monitor;
}
}
// 客户端代码
Computer computer = new Computer("Intel Core i7", "16GB", "1TB", "27 inch");
这个例子的问题在于,一旦我们需要构造一个更复杂的电脑,比如需要多个硬盘,或者需要额外的组件,如显卡、键盘、鼠标等,我们就需要增加更多的参数到构造函数中,如:
public Computer(String CPU, String memory, String hardDrive1, String hardDrive2, String monitor, String GPU, String keyboard, String mouse) {
// ...
}
// 客户端代码
Computer computer = new Computer("Intel Core i7", "16GB", "1TB", "2TB", "27 inch", "Nvidia RTX 3080", "Mechanical Keyboard", "Gaming Mouse");
可以看到,这种方式会使得代码非常冗长且难以阅读和理解。而且,如果有一些组件是可选的,我们还需要创建额外的构造函数来处理这些情况,这会导致代码冗余且难以维护。
此外,如果组件的组装顺序有一定的要求,例如首先需要安装CPU和内存,然后是硬盘和显示器,最后是其他的附加设备,这种顺序在上述代码中无法得到保证。
这就是为什么我们需要建造者模式。使用建造者模式,我们可以逐步构造对象,每一步都可以独立进行,且可以确保遵循正确的步骤和顺序。例如:
public class ComputerBuilder {
private Computer computer = new Computer();
public ComputerBuilder addCPU(String CPU) {
computer.setCPU(CPU);
return this;
}
public ComputerBuilderWithMemory addMemory(String memory) {
computer.setMemory(memory);
return new ComputerBuilderWithMemory(computer);
}
}
public class ComputerBuilderWithMemory {
private Computer computer;
public ComputerBuilderWithMemory(Computer computer) {
this.computer = computer;
}
public ComputerBuilderWithStorage addHardDrive(String hardDrive) {
computer.setHardDrive(hardDrive);
return new ComputerBuilderWithStorage(computer);
}
}
public class ComputerBuilderWithStorage {
private Computer computer;
public ComputerBuilderWithStorage(Computer computer) {
this.computer = computer;
}
public ComputerBuilderWithMonitor addMonitor(String monitor) {
computer.setMonitor(monitor);
return new ComputerBuilderWithMonitor(computer);
}
}
public class ComputerBuilderWithMonitor {
private Computer computer;
public ComputerBuilderWithMonitor(Computer computer) {
this.computer = computer;
}
public Computer build() {
return computer;
}
}
// 使用示例
Computer computer = new ComputerBuilder()
.addCPU("Intel Core i7")
.addMemory("16GB")
.addHardDrive("1TB")
.addMonitor("27 inch")
.build();
在这个例子中,如果你尝试不按顺序添加组件,编译器会报错,因为每个阶段的 Builder 类只提供了执行下一个步骤的方法。
例如,如果你创建了一个 ComputerBuilder
对象,并尝试直接添加硬盘,像这样:
ComputerBuilder builder = new ComputerBuilder();
builder.addHardDrive("1TB"); // 编译错误!
这会导致编译错误,因为 ComputerBuilder
类没有 addHardDrive
方法。只有在执行了 addCPU
和 addMemory
方法之后,返回的 ComputerBuilderWithMemory
类才有 addHardDrive
方法。
这就强制了一种顺序,你必须先添加 CPU,然后添加内存,然后添加硬盘,等等。这就是如何通过设计 API 来强制建造步骤的一种方式。
但是请注意,这并不是建造者模式的一部分,而是对它的一种可能的扩展。建造者模式的基本想法是将一个复杂对象的构建过程封装起来,使得对象可以被逐步构建,每个步骤可以独立进行。但是否以及如何强制特定的步骤顺序,这完全取决于你的具体需求。
不强制顺序的例子为,addCPU和addHardDrive等可以任意调换顺序
Computer computer = new ComputerBuilder()
.addCPU("Intel Core i7")
.addMemory("16GB")
.addHardDrive("1TB")
.addMonitor("27 inch")
.addGPU("Nvidia RTX 3080")
.addKeyboard("Mechanical Keyboard")
.addMouse("Gaming Mouse")
.build();
在表面上看,使用建造者模式和直接使用 setter 方法看起来很相似,但两者有一些重要的区别。
-
更好的可读性:对于具有多个配置选项的复杂对象,使用建造者模式可以提高代码的可读性。你可以清楚地看到对象的创建和配置是如何进行的。另一方面,一系列的 setter 方法调用可能会导致代码看起来凌乱和难以理解。
-
不变性:使用建造者模式,你可以创建不可变的对象。一旦对象被创建,就不能再改变它的状态。这对于需要确保对象状态不变的情况非常有用。使用 setter 方法,你无法保证这一点,因为任何人都可以在任何时候调用 setter 方法来改变对象的状态。
-
更安全的对象创建:使用建造者模式,你可以确保对象在所有必需的属性都被设置之前,不会被创建出来。这提高了代码的健壮性,因为你可以避免创建出不完整或不一致的对象。
-
更好的控制:建造者模式可以让你在对象创建过程中执行更多的逻辑,如参数验证、对象的初始化等。在 setter 方法中,这样的控制通常是不可能或很困难的。
总的来说,建造者模式在处理复杂对象创建和配置时更加强大和灵活。但是,如果你只是简单地设置几个属性,那么使用 setter 方法可能会更简单。你需要根据你的具体需求来选择最合适的方法。
"不变性"的解释
在编程中是一个非常重要的概念,它指的是一旦一个对象被创建,其状态就不能再被改变。这样可以提高程序的健壮性和安全性,因为你可以确保一旦一个对象被正确创建,它就永远不会处于一个无效的状态。
让我们以一个简单的 "Person" 类为例。这个类有两个属性:名字(name)和年龄(age)。在不使用建造者模式的情况下,你可能会这样使用这个类:
Person person = new Person();
person.setName("Alice");
person.setAge(25);
这里的问题是,有可能在调用 setName
和 setAge
之间,或者在调用这些方法之后,其他的代码会访问到 person
对象。在这种情况下,person
对象可能处于一个不完整或不一致的状态。
现在让我们使用建造者模式来改进这个例子:
Person person = new PersonBuilder()
.setName("Alice")
.setAge(25)
.build();
在这个版本中,person
对象在 build
方法调用之前都是不可见的。只有当所有必要的属性都被设置之后,build
方法才会创建一个新的 Person
对象。这样,你就可以确保无论何时访问 person
对象,它都是处于一个完整且一致的状态。
另外,如果 Person
类没有提供任何 setter 方法,那么一旦 person
对象被创建,就不能再改变它的状态。这就是所谓的 "不变性"。不可变对象可以使代码更容易理解和测试,因为你不需要考虑对象状态的改变可能带来的复杂性。
4、其它例子
建造者模式涉及到四个主要组成部分:产品(Product)、建造者(Builder)、具体建造者(Concrete Builder)和指挥者(Director)。在这个模式中,产品的构建是被分步进行的,每一步都是通过建造者接口(在Java中通常表现为一个有返回自身引用的方法,以支持链式调用)来完成的,具体建造者负责实现这些构建步骤,而指挥者则负责决定产品构建的步骤顺序。
Java的StringBuilder类
在StringBuilder的例子中,StringBuilder实例本身就扮演着“建造者”的角色。
-
产品(Product):这里的产品就是最终构建出的String字符串。
-
建造者(Builder)和具体建造者(Concrete Builder):StringBuilder类就扮演了这两个角色,它定义了一些方法(例如append()、insert()等)用于添加或修改字符串,这些方法都返回StringBuilder对象本身(即this),使得这些方法可以链式调用,每次调用都是在构建过程中添加一块“部件”。
-
指挥者(Director):在这个例子中,指挥者的角色通常由调用StringBuilder方法的客户端代码(即使用StringBuilder的程序员)来扮演,他们决定调用哪些方法以及调用的顺序。
虽然
StringBuilder
类没有.build()
方法,但它的工作方式仍然与建造者模式的核心思想相符。StringBuilder
的目的是逐步创建一个字符串。每次调用它的.append()
方法,你都在添加一个新的部分到最终的字符串中。这就是建造者模式的核心思想:将一个复杂对象的构建过程分解为一系列的步骤,这些步骤可以逐个进行,并且可以独立于其他步骤。至于
StringBuilder
为什么没有.build()
方法,这是因为它的.toString()
方法扮演了这个角色。当你调用.toString()
方法时,StringBuilder
会返回一个包含了所有添加的部分的新字符串。这就是建造过程的最后一步。另一方面,setter 方法通常用于修改对象的单个属性,而不是逐步构建新的对象。你可以在任何时候调用 setter 方法,而不需要关心其他的步骤。这和建造者模式的思想是不一样的。
总的来说,
StringBuilder
类是建造者模式的一个实例,尽管它没有明确的.build()
方法。但它的工作方式 —— 通过一系列的步骤逐步构建一个复杂的对象 —— 是符合建造者模式的思想的。针对上述解释,可能存在的疑问**“StringBuilder在toString()后还可以继续append(),这不是也和set方法一样。“在任何时候调用 setter 方法”,为什么还说是建造者模式呢”**
确实,从某种程度上看,StringBuilder的
append()
方法和常规setter方法有些类似,因为它们都可以在任何时候被调用,但还是存在一些关键的区别。首先,StringBuilder的
append()
方法并不修改一个已存在的字符串,它是在当前的StringBuilder实例上追加一个新的字符串片段。每次调用append()
方法,都是在为最终的字符串添加新的部分,这非常类似建造者模式中的 "添加步骤"。其次,StringBuilder的
append()
方法返回的是StringBuilder对象本身,这允许我们链式地调用多个append()
方法。这就是所谓的"流式API"或者"链式调用",这也是建造者模式的一种常见实现方式。然后,当你最后调用
toString()
方法时,它将所有通过append()
方法添加的字符串片段合并为一个完整的字符串,这就类似于建造者模式中的build()
方法。最后,不像常规setter方法,StringBuilder的
append()
方法并不对外暴露对象内部的细节,它提供了一种更为抽象和简洁的方式来构建字符串,这也符合建造者模式的设计理念。所以,尽管StringBuilder的
append()
方法在某些方面与常规的setter方法相似,但由于上述原因,它更多的是被看作是建造者模式的一种实现。
Android的AlertDialog.Builder类
在AlertDialog.Builder的例子中,Builder类扮演着“建造者”的角色。
- 产品(Product):这里的产品就是AlertDialog实例。
- 建造者(Builder)和具体建造者(Concrete Builder):AlertDialog.Builder类就扮演了这两个角色,它定义了一些方法(例如setTitle()、setMessage()、setPositiveButton()等)用于设置AlertDialog的属性,这些方法都返回AlertDialog.Builder对象本身,使得这些方法可以链式调用,每次调用都是在构建过程中添加一块“部件”。
- 指挥者(Director):在这个例子中,指挥者的角色通常由调用AlertDialog.Builder方法的客户端代码(即使用AlertDialog.Builder的程序员)来扮演,他们决定调用哪些方法以及调用的顺序。
总的来说,这两个例子都充分体现了建造者模式的思想:通过使用一个Builder对象,客户端可以一步步地构建一个复杂的对象,每一步都是独立的,且Builder提供了链式调用的支持,使得这个构建过程更加高效、灵活和易于理解。
5、工厂方法模式和建造者模式的区别
建造者模式和工厂方法模式都是创建型设计模式,用于处理对象创建的问题,但它们之间存在着一些关键的区别。
1. 创建的复杂性:
- 建造者模式主要用于创建复杂对象,这些对象的构建通常需要多个步骤,而且这些步骤之间的顺序可能会有所不同。建造者模式提供了一种方式来将这个复杂的构建过程分解为一系列的步骤,这些步骤可以逐个进行,这样可以让代码更清晰,也更易于维护和扩展。
- 相比之下,工厂方法模式通常用于创建简单的对象,这些对象的构建过程相对简单,通常只需要一步。工厂方法模式关注的是怎么在运行时动态地创建所需的对象,而不需要知道这些对象的具体类别。
2. 返回的对象类型:
- 建造者模式通常总是返回相同类型的对象,只是这些对象的内部状态(例如内部的数据或者配置)可能会不同。
- 相比之下,工厂方法模式可以返回不同类型的对象,只要这些对象共享同一个接口或者基类。这让我们可以在运行时根据条件动态地决定要创建哪种类型的对象。
3. 使用场景:
- 建造者模式通常用在构建过程比较复杂、需要多步骤才能完成的场景,例如组装一台电脑或者创建一个复杂的HTML文档等。
- 相比之下,工厂方法模式适合于那些你知道你需要一个对象,但是你不知道这个对象应该是什么类型的场景。例如,在一个图形编辑器程序中,你可能需要创建一个图形对象,但是这个对象到底应该是圆形、矩形还是多边形,这取决于用户的选择。
这就是建造者模式和工厂方法模式的主要区别。需要注意的是,虽然这两种模式都可以用来创建对象,但是它们解决的问题和应用的场景是不同的。选择哪种模式取决于你面临的具体问题和需求。