设计模式:Builder模式

在构建一些结构复杂的物体,比如建造一个大楼,没法一气呵成,只能先打牢地基,搭建框架,然后一层一层地盖;因此需要先构建组成这个物体的各个部分,然后分阶段给组装起来

如果需要将一个复杂对象的构建和表示分离,使得同样的构建过程可以创建不同的表示的意图时,可以使用Builder构建模式,也叫生成器模式;用Builder模式,用户只需要指定需要构建的类型就可以得到它们,而具体构建的过程和细节就不需要知道了

来一个示例,通过Builder模式编写一个文档,包括一个文档标题,字符串,和若干条目

定义的类如下

Builder:定义了决定文档结构的方法的抽象类

Director:编写一个文档类,用Builder的方法编写一个具体的类

TextBuilder:使用纯文本,字符串来编写文档

HTMLBuilder:使用HTML编写文档

Main:main,测试类

Builder抽象类,不做任何实际处理,由子类实现具体处理

首先是Builder类,声明编写文档的抽象类,定义makeTitle,makeString,makeTimes抽象方法编写标题,字符串和条目,close方法最终完成文档编写

package builder;

/**
* Copyright (C), 2014-2018, maoxiaomeng.com
* FileName: Builder
* Author: lihui
* Date: 2018/5/3 16:14
*/

public abstract class Builder {
public abstract void makeTitle(String title);
public abstract void makeString(String str);
public abstract void makeItems(String[] items);
public abstract void close();
}

接着是Director类,主要是用Builder抽象类里声明的方法来编写文档

Director类的构造函数的参数传入的是Builder类型,但是肯定不是Builder类的实例,因为Builder是抽象类无法实例化,传入的是Builder类的子类的实例,而这些Builder类的子类决定了编写出的文档的形式

construct方法是编写文档的方法;调用该方法就会编写文档;该方法会用到Builder类中声明的方法阿莱构建

package builder;

/**
* Copyright (C), 2014-2018, maoxiaomeng.com
* FileName: Director
* Author: lihui
* Date: 2018/5/3 16:37
*/

public class Director {
private Builder builder;
public Director(Builder builder) {
this.builder = builder;
}

public void construct() {
builder.makeTitle("Greeting");
builder.makeString(" 从早上至下午 ");
builder.makeItems(new String[]{
" 早上好 ",
" 下午好 ",
});
builder.makeString(" 晚上 ");
builder.makeItems(new String[]{
" 晚上好 ",
" 晚安 ",
" 再见 ",
});
builder.close();
}
}

接着是TextBuilder类,继承了Builder类,功能是使用纯文本编写文档,以String返回结果

package builder;

/**
* Copyright (C), 2014-2018, maoxiaomeng.com
* FileName: TextBuilder
* Author: lihui
* Date: 2018/5/3 16:19
*/

public class TextBuilder extends Builder {
private StringBuilder buffer = new StringBuilder();
public void makeTitle(String title) {
buffer.append("======================================\n");
buffer.append("<<" + title + ">>\n");
buffer.append("\n");
}

public void makeString(String str) {
buffer.append('#' + str + '\n');
buffer.append("\n");
}

public void makeItems(String[] items) {
for (int i = 0; i < items.length; i++) {
buffer.append(" *" + items[i] + "\n");
}
buffer.append("\n");
}

public void close() {
buffer.append("======================================\n");
}

public String getResult() {
return buffer.toString();
}
}

还有一个HTMLBuilder类,也是Builder类的子类,功能是使用HTML编写文档,返回HTML文件的名字

package builder;

import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;

/**
* Copyright (C), 2014-2018, maoxiaomeng.com
* FileName: HTMLBuilder
* Author: lihui
* Date: 2018/5/3 16:26
*/

public class HTMLBuilder extends Builder {
private String filename;
private PrintWriter writer;
public void makeTitle(String title) {
filename = title + ".html";
try {
writer = new PrintWriter(new FileWriter(filename));
} catch (IOException e) {
e.printStackTrace();
}
writer.println("<html><head><title>" + title + "</title></head><body>");
writer.println("<h1>" + title + "</h1>");
}

public void makeString(String str) {
writer.println("<p>" + str + "</p>");
}

public void makeItems(String[] items) {
writer.println("<ul>");
for (int i = 0; i < items.length; i++) {
writer.println("<li>" + items[i] + "</li>");
}
writer.println("</ul>");
}

public void close() {
writer.println("</body></html>");
writer.close();
}

public String getResult() {
return filename;
}
}

最后是main

当命令行参数传入plain时,会将TextBuilder类的实例作为参数传递到Director类的构造函数中;如果是html就穿HTMLBuilder类的实例

因此必须在Builder中声明足够多的方法,以实现编写文档的功能,但并不包括TextBuilder和HTMLBuilder中特有的方法

package builder;

/**
* Copyright (C), 2014-2018, maoxiaomeng.com
* FileName: Main
* Author: lihui
* Date: 2018/5/3 16:31
*/

public class Main {
public static void main(String[] args) {
if (args.length != 1) {
usage();
System.exit(0);
}
if (args[0].equals("plain")) {
TextBuilder textBuilder = new TextBuilder();
Director director = new Director(textBuilder);
director.construct();
String result = textBuilder.getResult();
System.out.println(result);
} else if (args[0].equals("html")) {
HTMLBuilder htmlBuilder = new HTMLBuilder();
Director director = new Director(htmlBuilder);
director.construct();
String filename = htmlBuilder.getResult();
System.out.println(filename + " 文件编写完成 ");
} else {
usage();
System.exit(0);
}
}

public static void usage() {
System.out.println("Usage: java Main plain 编写纯文本文档");
System.out.println("Usage: java Main html 编写HTML文档");
}
}

 

Builder模式的角色:

Builder:构造者,例子中的Builder类;负责定义用于生成实例的接口API方法

ConcreteBuilder:具体的构造者,例子中的TextBuilder类和HTMLBuilder类;负责实现Builder角色的接口的类

Director:监工,例子中的Director类;负责使用Builder角色的接口来生成实例,并不依赖ConcreteCreator角色;为了确保不论ConcreteCreator角色是如何被定义的,Director角色都能正常工作,它只调用在Builder角色中被定义的方法

Client:调用者,例子中的Main类;

 

流程思考:

整个Main里最关键的两行代码,一个是将子类对象传参到Director的构造函数里,赋给超类的引用;另一个就是调用construct方法,由于多态,而超类Builder正好是一个抽象类,必须被子类重写,因此调用的方法其实就是传入的子类对应的方法

下面内容重点理解:

这里Main类并不知道Builder类,也没有直接调用,它只是调用了Director类的construct方法,这样Director类就会开始工作完成文档的编写;而Director类是知道Builder类的,它调用Builder类的方法完成文档的编写,但是它并不知道它真正使用的是哪个Builder的子类,不过也没必要知道,因为Director类只使用了Builder类的方法,而Builder类的子类都已经实现了这些方法

Director类不知道自己使用的究竟是Builder类的哪个子类,这样也好,因为只有不知道子类才能替换,不论将TextBuilder的实例传递给Director,还是将HTMLBuilder类的实例传递给Director,它都可以正常工作,原因正是Director类不知道Builder类的具体子类

正是因为不知道才看可以替换,正是因为可以替换,组件才具有高价值,作为设计人员,时刻需要关注这种可替换性

发表回复