建造者模式的两种不同实现方式
创建一个对象最常用的方法就是通过 new
关键字创建出一个对象,但是有的时候这种创建对象的方法并不适用。
建造者模式(又称为构建者模式或生成器模式),属于创建型模式,它用于组装复杂的实例。它可将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。用户只需要指定需要建造的类型就可以 得到该类型对应的产品实例 , 不关心建造过程细节。
建造者模式
建造者模式有两种写法,一种是 GoF 中提出的传统的建造者模式写法,一种是现在常用的不带主管类的写法。
不同的实现方式
GoF 中的建造者模式
这里介绍的建造者模式的实现接近于 GoF 的思想。
登场角色:
Product
(产品):我们需要获得的类。Builder
(抽象建造者):定义用于生成实例的接口(API),准备用于生成实例的方法。ConcreteBuilder
(具体的建造者):负责实现Builder
角色接口(API)的类。ConcreteBuilder 还定义了获取最终生成结果(产品)的方法。Director
(监工/指挥者):负责使用Builder
角色的接口(API)来生成实例,它并不依赖于ConcreteBuilder
角色(它只调用在Builder
角色中被定义的方法)。注意是具体类。组装的具体过程隐藏在Director
中。Client
(使用者):使用建造者模式。
在 GoF 书中没有 Client
和 Product
这两个角色,这里只是为了方便理解而引入。
建造者代码如下所示:
1 | public class Product { |
建造者模式的时序图:
sequenceDiagram
participant C as Client
participant D as Director
participant B as ConcreteBuilder
activate C
C ->> B : new
activate B
deactivate B
C ->> D : build
activate D
D ->> B : buildPart1
activate B
B -->> D :
deactivate B
D ->> B : buildPart2
activate B
B -->> D :
deactivate B
D ->> B : buildPart3
activate B
B -->> D :
deactivate B
D -->> C :
deactivate D
C ->> B : getProduct
activate B
B -->> C :
deactivate B
deactivate C
在建造者模式中,Main
不知道 Builder
类,Director
不知道 ConcreteBuilder
类。可替换性就体现在「只有不知道子类才能替换」。
关于主管类 Director
对于上面建造者模式写法,不同文章中的实现也有所不同:
- 有些写法是使用
Director
获取产品。这种方法让Director
完全控制构建流程,Builder
只负责部件构造,构建的代码部分实现了复用。用户只需和Director
交互。 - 有些写法是像上面文章讲的那样通过
ConcreteBuilder
获取产品,由客户控制建造流程。这种方式会呈现一定的构造灵活度。
严格来说,程序中并不一定需要主管类。客户端代码可直接以特定顺序调用创建步骤。不过,主管类中非常适合放入各种例行构造流程,以便在程序中反复使用。
对于客户端代码来说,主管类完全隐藏了产品构造细节。客户端只需要将一个生成器与主管类关联,然后使用主管类来构造产品, 就能从生成器处获得构造结果了。
建造者模式常用写法——省略 Director 角色
建造者模式的常用用法,是一种混合模式。
假设我们需要定义一个资源池配置类 ResourcePoolConfig
。这里的资源池可以简单理解为线程池、连接池、对象池等。包含以下参数:
name
:资源名称maxTotal
:最大总资源数量maxIdle
:最大空闲资源数量minIdle
:最小空闲资源数量
我们的构造函数或许是这样的:
1 | public class ResourcePoolConfig { |
不管可选或必选配置项,我们都用构造方法配置这些参数时,就叫做折叠构造函数模式(Telescoping Constructor Pattern)。
1 | public class ResourcePoolConfig { |
我们发现一些问题:
- 当
ResourcePoolConfig
的必选配置项变为 8 个、10 个时我们的构造函数会越来越长 - 当出现多个可选配置项时,我们构造函数的重载方法会越来越多。如果一些参数类型几乎都相似,那么参数传递时很容易传混或出错。
对于可选配置项的一个简单的解决方式为使用 setXXX()
函数来给成员变量赋值,以替代冗长繁琐的构造函数。这也是所谓的 Java Bean 模式。
1 | public class ResourcePoolConfig { |
但使用 setXXX()
函数还存在一些问题:
- 必填的配置不能可以使用
setXXX()
函数,因为如果各个必填配置存在互相依赖校验,这种校验逻辑无法实现。所以必选项配置的问题还是没有解决。 - 不能保证参数传递的先后次序。
- 一旦必填的配置多了起来,构造参数的列表还是会长起来。
- 如果我们希望
ResourcePoolConfig
是不可变的对象,那么setXXX()
就会出现问题。
为解决上述问题,Builder 模式诞生。
代码如下:
1 | public class Product{ |
使用方式:
1 | // 链式调用 |
与经典的建造者模式相比,这种模式把 Director
角色给省略了,另外在内部静态 Builder
的构建方法中返回了 this
自身,方便流式调用。
建造者模式小结
应用场景:
- 避免「重叠构造函数 (telescoping constructor)」的出现
- 创建不同形式的产品(制造过程相似且仅有细节上的差异)
- 构造复杂对象。
优点 | 缺点 |
---|---|
分步创建对象,暂缓创建步骤或递归运行创建步骤。 | 应用该模式时需要增加多个类。 |
生成不同形式的产品时,可以复用相同的制造代码。 | |
单一制作原则。将复杂构造代码从产品的业务逻辑中分离出来。 |
模式的对比与选用
相关的设计模式:
- 站内文章组合模式:创建复杂组合模式树时可以使用生成器,因为这可使其构造步骤以递归的方式运行。
- 站内文章门面模式:
- 在建造者模式中,
Director
角色通过组合Builder
角色中的复杂方法向外部提供可以简单生成实例的接口(API)(相当于示例程序中的construct
方法)。 - Facade 模式中的 Facade 角色则是通过组合内部模块向外部提供可以简单调用的接口(API)。
- 在建造者模式中,
- 站内文章桥接模式:主管类负责抽象工作,各种不同的生成器负责实现工作。
- 站内文章单例模式:抽象工厂模式、建造者模式和原型模式都可以用单例模式来实现。
建造者模式与工厂模式的对比
比较:
- 建造者模式:让建造者类来负责对象的创建工作。
- 站内文章工厂模式:工厂类负责对象的创建工作。创建不同但相关类型的对象(继承同一父类或接口
Product
的一组子类),由给定参数决定创建哪种类型的对象。Factory 抽象类定义的是一个实例产生的具体流程,子类实现具体的处理。
模式选用:
- 当创造一个对象需要很多步骤时 , 适合使用建造者模式
- 当创造一个对象只需要一个简单的方法就可以完成,适合使用工厂模式
建造者模式与抽象工厂模式的对比
建造者模式和抽象工厂模式都用于生成复杂的实例。
- 建造者模式则是分阶段地制作复杂实例,允许你在获取产品前执行一些额外构造步骤。
- 站内文章抽象工厂模式 通过调用抽象产品的接口(API)来组装抽象产品,生成具有复杂结构的实例。
- 马上返回产品。
- 抽象工厂模式强调的是产品族的创建,即相关的产品一起被创建出来,而建造者模式强调的是一个复杂对象的创建,即它的各个部分逐步被创建出来。
Lombok 中的 @Builder
注解
Lombok 是一个常用的 Java 库,通过添加注解可自动实现建造者模式代码或 Getter、Setter 等。其中,@Builder
注解可以用来实现建造者模式,省略手动编写建造者模式的代码。
1 | import lombok.Builder; |
使用方式:
1 | Person person = Person.builder() |
后记
🎂今天是博主的生日~
等待搞明白的问题:
- 问:GoF 中的建造者模式的具体应用在哪里?它能解决构造参数中的必选参数和可选参数的问题吗?答:它们之间的区别仅是有无主管的区别,建造者模式解决的问题就是为了避免「重叠构造函数 (telescoping constructor)」的出现。
参考与归档
本文 PlantUML 代码归档
1 | class Client |
参考文章
- 《图解设计模式》第七章 Builder 模式
- 极客时间专栏 - 设计模式之美 - 王争
- 设计模式第10讲——建造者模式(Builder)-CSDN博客
- 秒懂设计模式之建造者模式(Builder pattern) - 知乎
- 建造者模式(Builder Pattern)详解-CSDN博客
- 这篇文章可能有误,请仔细甄别:Java设计模式:建造者模式之经典与流式的三种实现(四)-阿里云开发者社区
- 原型设计模式