一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情。
模式动机
无论在现实世界还是软件系统中,总存在一些复杂的对象,它们拥有多个组成部分。例如,计算机是由 CPU、主板、内存、硬盘、显卡、机箱、显示器、键盘、鼠标等部件组装而成的;如果由用户自行构造计算机,那么在构造时需要对诸多成员变量和嵌套对象进行繁杂的初始化工作,这些初始化代码通常深藏于一个包含众多参数且基本让人看不懂的构造函数中。
而建造者模式存在的目的,就是让大多数用户根本无须知道这些部件的装配细节,直接要得到组装后的计算机。
再比如,我们来思考下如何创建一个房屋 House 对象。
首先创建一栋简单的房屋(四面墙和地板、房门、窗户以及屋顶),但这时你想要一栋带有游泳池的房子,还想要院子等其他设施,怎么办?你可以直接在 House 基类中创建一个包括所有可能参数的超级构造函数,并用它来控制 House 对象。
这似乎是我们平常最容易想到的解决办法了,但它会造成一个问题:通常情况下,绝大部分参数都没有使用,这使得对于构造函数的调用十分不简洁。
不直接使用构造函数创建复杂对象的另一个原因:在复杂对象中,可能还存在一些限制条件,如某些属性没有赋值则复杂对象不能作为一个完整的产品使用;有些属性的赋值必须按照某个顺序,一个属性没有赋值之前,另一个属性可能无法赋值等。
这时我们有另一种解决方案,将对象构造代码从产品类中抽取出来,并将其放在一个名为建造者/生成器的独立对象中。
建造者模式可以让你分步骤创建复杂的房屋对象,而且无需调用所有步骤,只需调用创建特定对象配置所需的那些步骤即可。
当你需要以为相同的步骤创建不同形式的 House 对象时(如木屋房门用木头建造,城堡房门用石头建造),那么就需要创建多个不同的具体生成器来实现抽象生成器类,用不同的方式实现一组相同的创建步骤(多态性)。
你可以进一步免去用户调用生成器类中步骤的过程,而将其交给主管类 Director,由它定义创建步骤的执行顺序,而生成器则提供这些步骤的实现。其实严格来说,并不一定需要 Director 类,用户也可以直接以特定顺序调用生成器中的创建步骤。
对于用户而言,主管类 Director 完全隐藏了产品构造细节,用户只需要调用主管类,由主管类从生成器处获得构造后的对象并返回给用户。
⭐以上从实际场景出发,描述了一个完整的建造者模式!
定义
建造者模式又称生成器模式,它属于创建型模式。
建造者模式让你能够分步骤创建复杂对象,允许你使用相同的创建代码生成不同类型和形式的对象;而用户无须关心该对象所包含的属性以及装配顺序。
UML 类图
模式结构
建造者模式包含如下角色:
Builder:生成器接口声明在所有子类生成器中通用的产品构造步骤ConcreteBuilder:具体建造者/具体生成器提供构造过程的不同实现,也可以构造不遵循通用接口的产品Product:最终生成的产品对象Director:指挥者定义调用构造步骤的顺序,这样就可以创建和复用特定的产品配置
更多实例
示例代码
Product.java
public class Product {
private String partA;
private String partB;
private String partC;
public String getPartA() {
return partA;
}
public void setPartA(String partA) {
this.partA = partA;
}
public String getPartB() {
return partB;
}
public void setPartB(String partB) {
this.partB = partB;
}
public String getPartC() {
return partC;
}
public void setPartC(String partC) {
this.partC = partC;
}
@Override
public String toString() {
return "Product{" +
"partA='" + partA + '\'' +
", partB='" + partB + '\'' +
", partC='" + partC + '\'' +
'}';
}
}
Builder.java
public abstract class Builder {
protected Product product = new Product();
public abstract void buildPartA();
public abstract void buildPartB();
public abstract void buildPartC();
public Product getResult() {
return this.product;
}
}
ConcreteBuilder1.java
public class ConcreteBuilder1 extends Builder {
@Override
public void buildPartA() {
product.setPartA("A1");
}
@Override
public void buildPartB() {
product.setPartB("B1");
}
@Override
public void buildPartC() {
product.setPartC("C1");
}
}
ConcreteBuilder2.java
public class ConcreteBuilder2 extends Builder {
@Override
public void buildPartA() {
product.setPartA("A2");
}
@Override
public void buildPartB() {
product.setPartB("B2");
}
@Override
public void buildPartC() {
product.setPartC("C2");
}
}
Director.java
该类主要有两个作用:一方面隔离了客户与生产过程;另一方面它还负责把控产品的生成过程。
指挥者 Director 针对抽象建造者编程,客户端只需要知道具体建造者的类型,即可通过指挥者类调用建造者的相关方法,返回一个完整的产品对象。
public class Director {
private Builder builder;
public Director(Builder builder) {
this.builder = builder;
}
public void changeBuilder(Builder builder) {
this.builder = builder;
}
public Product make() {
builder.buildPartB();
builder.buildPartA();
builder.buildPartC();
return builder.getResult();
}
}
Client.java
用户类
public class Client {
public static void main(String[] args) {
Director director = new Director(new ConcreteBuilder1());
// Using director to build a complete product
Product product1 = director.make();
System.out.println(product1);
// ChangeBuilder
director.changeBuilder(new ConcreteBuilder2());
Product product2 = director.make();
System.out.println(product2);
}
}
优缺点
✔客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。
✔每一个具体建造者相互独立,可以随时替换或新增,用户使用不同的具体建造者即可获得不同的产品对象;一旦增加新的具体建造者,无须修改原有类库代码,系统扩展方便,符合“开闭原则”。
✔你可以将复杂构造代码从产品的业务逻辑中分离出来,符合“单一职责原则”。
❌建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适用建造者模式,因此其使用范围受到一定的限制。
❌如果产品内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,从而导致系统变得很庞大。
适用场景
在以下情况可以使用建造者模式:
(1)需要生成的产品对象的属性相互依赖,需要指定其生成顺序。
(2)当需要创建各种形式的产品,它们的制造过程相似且仅有细节上的差异。
(3)隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品。
(4)需要生成的产品对象有复杂的内部结构,这些产品对象通常包含多个成员属性,使用建造者模式以避免“重叠构造函数”的出现。
假设你的构造函数有十来个可选参数,调用该函数会非常不方便;因此你需要重载这个构造函数(如下),但这些构造函数仍然需要调用主构造函数,传递一些默认参数来替换省略掉的参数。
只有在 C# 或 Java 等支持重载的编程语言才能写出如此复杂的构造函数
class Pizza {
Pizza(int size) { ... }
Pizza(int size, boolean cheese) { ... }
Pizza(int size, boolean cheese, boolean pepperoni) { ... }
// ...
建造者模式让你可以分步骤生成对象,而且允许你仅使用必须的步骤 (Director 控制)。应用该模式后,你再也不需要将几十个参数塞进构造函数里了。
「建造者模式」落地
(1)JavaMail:一步步构建一个完整的邮件对象
(2)在很多游戏软件中,地图包括天空、地面、背景等组成部分,人物角色包括人体、服装、装备等组成部分,可以使用建造者模式对其进行设计,通过不同的具体建造者创建不同类型的地图或人物。
(3)在 JDK 中得到了广泛的应用:
java.lang.StringBuilder#append()java.lang.StringBuffer#append()java.lang.Appendable的所有实现
识别方式:生成器模式可以通过类来识别,它拥有一个构建对象方法和多个配置对象的方法。生成器方法通常支持方法链式调用,如:
someBuilder.setValueA(1)
.setValueB(2)
.create();
模式扩展
建造者模式的简化
(1)省略抽象建造者 Builder 角色:如果系统只有一个具体建造者的话,可以省略抽象建造者。
(2)省略指挥者 Director 角色:在具体建造者只有一个的情况下,如果抽象建造者角色已经被省略了,那么还可以省略指挥者角色,让 ConcreteBuilder 角色扮演指挥者与建造者双重角色。
「建造者模式」与「工厂模式」
从以下几个不同的维度来比较:
建造者模式与工厂模式的关注点不同,但两者也可以结合使用
- 建造者模式用于创建复杂对象;工厂模式用于创建简单对象。
- 建造者模式注重零部件的组装过程,返回的是一个完整产品;工厂模式注重零部件的创建过程,返回的是部件。好比将工厂模式看成汽车配件生产工厂,而建造者模式看成汽车组装工厂。
- 工厂模式中,客户端实例化工厂类,然后调用工厂方法获取所需对象;而在建造者模式中,客户端可以不直接调用建造者相关方法,而是通过指挥者来指导如何生成对象,它更侧重一步步构建一个复杂对象,返回一个完整的对象。
最后
👆上一篇:「设计模式」🛕抽象工厂模式(Abstract Factory)
❤️ 好的代码无需解释,关注「手撕设计模式」专栏,跟我一起学习设计模式,你的代码也能像诗一样优雅!
❤️ / END / 如果本文对你有帮助,点个「赞」支持下吧,你的支持就是我最大的动力!