生成器 Builder

生成器模式也叫“建造者模式”是一种创建型设计模式, 使你能够分步骤创建复杂对象。 该模式允许你使用相同的创建代码生成不同类型和形式的对象。


为什么要使用?

生成器模式的对象职责:

  • 分步骤创建复杂对象。分阶段、分步骤的方法更适合多次运算结果类创建场景。在实际开发中,并非所有参数都能一次性准备好的,需要通过其他运算后才能得出。这时我们有 3 种方法可以实现:

    1. 构造函数:如果参数很多,并且有许多不必要生成的参数,那么我们就需要根据各种情况,编写各种构造函数,或者在全参构造函数中为那些不必要的参数传值。代码臃肿、多余。

    2. Setter 方法:如果使用 Setter 方法,就需要用无参构造先创建对象,再一个个 Setter 属性,如果属性过多,而且要生成的对象多,也是很麻烦、冗余。

    3. 生成器模式:生成器模式可以帮助你分步骤的创建一个复杂对象,虽然生成器模式本身会占用一定的资源。但如果要创建很多对象或创建的对象很复杂,这点资源也可以忽略不计。例如创建一个学生对象:

      1
      Student student = new Student.Builder().name("张三").age(18).sex("男").build();
  • 使用相同的创建代码生成不同类型和形式的对象。不需要关系特定类型的建造者的具体算法实现。例如很多框架都会使用生成器模式去创建对象,我们在使用这些框架去创建某些对象时,不需要关注其内部逻辑,只需要着重关注它能给我们带来什么功能,这样能提高开发效率。


模式结构

  • 生成器 (Builder) 接口声明在所有类型生成器中通用的产品构造步骤。
  • 具体生成器 (Concrete Builders) 提供构造过程的不同实现。 具体生成器也可以构造不遵循通用接口的产品。
  • 产品 (Products) 是最终生成的对象。 由不同生成器构造的产品无需属于同一类层次结构或接口。
  • 主管 (Director) 类定义调用构造步骤的顺序, 这样你就可以创建和复用特定的产品配置。
  • 客户端 (Client) 必须将某个生成器对象与主管类关联。 一般情况下, 你只需通过主管类构造函数的参数进行一次性关联即可。 此后主管类就能使用生成器对象完成后续所有的构造任务。 但在客户端将生成器对象传递给主管类制造方法时还有另一种方式。 在这种情况下, 你在使用主管类生产产品时每次都可以使用不同的生成器。

生成器模式的类图:

生成器模式的顺序图:


模式实现

该示例通过使用生成器模式分别创建复杂对象 HTML 页面和 Markdown 文档。

示例程序的类图

代码实现

文档生成器 Builder
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/** 生成器接口,统一文档编写接口 */
public interface Builder {
/**
* 创建标题
* @param title 标题内容
*/
void makeTitle(String title);

/**
* 创建无序列表
* @param items 列表项
*/
void makeItems(String[] items);

/**
* 创建内容
* @param content 内容
*/
void makeContent(String content);
}
HTML生成器 HTMLBuilder
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/** HTML文档实现类 */
public class HTMLBuilder implements Builder {
private String html = "";
/**
* 创建标题
* @param title 标题内容
*/
@Override
public void makeTitle(String title) {
html += "<html><head><title>" + title + "</title></head></html>" + "\n";
}

/**
* 创建无序列表
* @param items 列表项
*/
@Override
public void makeItems(String[] items) {
html += "<ul>" + "\n";
for (String item : items) {
html += "<li>" + item + "</li>" + "\n";
}
html += "</ul>" + "\n";
}

/**
* 创建内容
* @param content 内容
*/
@Override
public void makeContent(String content) {
html += "<p>" + content + "</p>" + "\n";
}

/**
* 获取HTML文档
* @return HTML文档字符串
*/
public String getResult() {
return html;
}
}
Markdown文档生成器 MarkdownBuilder
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/** markdown文档实现类 */
public class MarkdownBuilder implements Builder {
private String markdown = "";
/**
* 创建标题
* @param title 标题内容
*/
@Override
public void makeTitle(String title) {
markdown += "## " + title + "\n";
}

/**
* 创建无序列表
* @param items 列表项
*/
@Override
public void makeItems(String[] items) {
for (String item : items) {
markdown += "* " + item + "\n";
}
}

/**
* 创建内容
* @param content 内容
*/
@Override
public void makeContent(String content) {
markdown += content + "\n";
}

/**
* 获取markdown文档
* @return markdown文档字符串
*/
public String getResult() {
return markdown;
}
}
编写文档主管 Director
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/** 主管类,决定生成的文档类型 */
public class Director {
/** 生成器 */
private Builder builder;

/** 注入生成器 */
public Director(Builder builder) {
this.builder = builder;
}

/** 创建一个文档 */
public void construct() {
builder.makeTitle("这是一个标题");
String[] items = {"无序列表1", "无序列表2", "无序列表3"};
builder.makeItems(items);
builder.makeContent("这是一段内容!");
}
}

代码测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/** 测试生成器模式 */
public class Test {
public static void main(String[] args) {
System.out.println("用HTMLBuilder生成文档");
HTMLBuilder htmlBuilder = new HTMLBuilder();
Director director1 = new Director(htmlBuilder);
director1.construct();
System.out.println(htmlBuilder.getResult());

System.out.println("----------------- 分割线 -----------------\n");

System.out.println("用MarkdownBuilder生成文档");
MarkdownBuilder markdownBuilder = new MarkdownBuilder();
Director director2 = new Director(markdownBuilder);
director2.construct();
System.out.println(markdownBuilder.getResult());
}
}

输出结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
用HTMLBuilder生成文档
<html><head><title>这是一个标题</title></head></html>
<ul>
<li>无序列表1</li>
<li>无序列表2</li>
<li>无序列表3</li>
</ul>
<p>这是一段内容!</p>

----------------- 分割线 -----------------

用MarkdownBuilder生成文档
## 这是一个标题
* 无序列表1
* 无序列表2
* 无序列表3
这是一段内容!

我们也可以直接跳过 Director,直接使用 Builder,或者在 Director 里编写多个方法,注入不同 Builder


常用场景和解决方案

  • 需要生成的对象包含多个成员属性,使用生成器模式可以避免“重叠构造函数”和“繁琐 Setter”的出现。
  • 需要生成的对象的属性相互依赖,需要指定其生成顺序。
  • 对象的创建过程独立于创建该对象的类。
  • 需要隔离复杂对象的创建和使用,并使用相同的创建过程可以创建不同的产品。

模式的优缺点

优点 缺点
你可以分步创建对象, 暂缓创建步骤或递归运行创建步骤。 由于该模式需要新增多个类, 因此代码整体复杂程度会有所增加。
生成不同形式的产品时, 你可以复用相同的制造代码。
单一职责原则。 你可以将复杂构造代码从产品的业务逻辑中分离出来。

使用生成器模式的优势

  • 分离创建与使用,使用方不需要知道类的内部实现逻辑细节,通过统一接口调用,可以组合出不同类型的对象。
  • 满足开闭原则,每一个生成器都相对独立,可以很容易替换或新增,提高代码的可拓展性。
  • 自由地组合对象的创建过程,使用者可以使用少量代码灵活创建满足自己需求的对象。

使用生成器模式的劣势

  • 使用范围有限。
  • 容易引起超大的类。
  • 增加代码行数。

拓展知识

  • 生成器重点关注如何分步生成复杂对象。 抽象工厂专门用于生产一系列相关对象。 抽象工厂会马上返回产品, 生成器则允许你在获取产品前执行一些额外构造步骤。
  • 你可以结合使用生成器和桥接模式:主管类负责抽象工作,各种不同的生成器负责实现工作。
  • 抽象工厂、 生成器和原型都可以用单例模式来实现。
  • 设计时要谨记“只有不知道子类才能替换”。


🔙 设计模式

📌最后:希望本文能够给您提供帮助,文章中有不懂或不正确的地方,请在下方评论区💬留言!

🔗参考文献:

🌐 设计模式 –refactoringguru

▶️ bilibili-趣学设计模式;黄靖锋. –拉勾教育

📖 图解设计模式 /(日)结城浩著;杨文轩译. –北京:人民邮电出版社,2017.1