一、什么是Builder模式
大都市中林立着许多高楼大厦,这些高楼大厦都是具有建筑结构的大型建筑。通常,建造和构建这种具有建筑结构的大型物体在英文中称为Build。
在建造大楼时,需要先打牢地基,搭建框架,然后自下而上地一层一层盖起来。通常,在建造这种具有复杂结构的物体时,很难一气呵成。我们需要首先建造组成这个物体的各个部分,然后分阶段将它们组装起来。
用一句话来概括:Builder模式用于组装具有复杂结构的实例。
二、Builder模式示例代码
这是一段使用Builder模式编写“文档”的程序。这里编写出的文档含有一个标题、几个字符串、条目项目。
Builder类中定义了决定文档结构的方法,然后Director类使用该方法编写一个具体的文档。
Builder是抽象类,它并没有进行任何实际的处理,仅仅声明了抽象方法。Builder类的子类决定了用来编写文档的具体处理。
在示例程序中,我们定义了以下Builder类的子类。
TextBuilder类:使用纯文本(普通字符串)编写文档HTML.Builder类:使用HTML 编写文档
Director使用TextBuilder类时可以编写纯文本文档;使用HTMLBuilder类时可以编写HTML文档。
2.1 各个类之间的关系
各个类的功能:
类图:
2.2 Builder类
Builder类是一个声明了编写文档的方法的抽象类。
public abstract class Builder { /** * 编写标题 */ public abstract void makeTitle(String title); /** * 编写字符串 */ public abstract void makeString(String str); /** * 编写条目 * @param items */ public abstract void makeItems(String[] items); /** * 完成编写 */ public abstract void close(); }
2.3 Director类
Director类使用Builder类中声明的方法来编写文档。
Director类的构造函数的参数是Builder类型的。但是实际上我们并不会将Builder类的实例作为参数传递给Director类。这是因为Builder类是抽象类,是无法生成其实例的。实际上传递给Director类的是Builder类的子类的实例。而正是这些Builder类的子类决定了编写出的文档的形式。
construct方法是编写文档的方法。调用这个方法后就会编写文档。construct方法中所使用的方法都是在Builder类中声明的方法( construct的意思是“构建”)。
public class Director { private Builder 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(); } }
2.4 TextBuilder类
TextBuilder类是Builder类的子类,它的功能是使用纯文本编写文档,并以string返回结果。
public class TextBuilder extends Builder { //文档内容 private StringBuffer buffer = new StringBuffer(); /** * 纯文本的标题 */ @Override public void makeTitle(String title) { buffer.append("==============================\n"); buffer.append("[" + title + "]\n"); buffer.append("\n"); } /** * 纯文本的字符串 */ @Override public void makeString(String str) { buffer.append('■' + str + "\n"); buffer.append("\n"); } /** * 纯文本的条目 */ @Override public void makeItems(String[] items) { for (int i = 0; i < items.length; i++) { buffer.append(" ▪" + items[i] + "\n"); } buffer.append("\n"); } /** * 完成文档 */ @Override public void close() { buffer.append("==============================\n"); } /** * 完成的文档 */ public String getResult() { return buffer.toString(); } }
2.5 HTMLBuilder类
HTMLBuilder类也是Builder类的子类,它的功能是使用HTML编写文档,其返回结果是HTML文件的名字。
public class HTMLBuilder extends Builder{ //文件名 private String filename; private PrintWriter writer; /** * HTML文件的标题 */ @Override public void makeTitle(String title) { filename = title + ".html"; try { new PrintWriter(new FileWriter(filename)); } catch (IOException e) { e.printStackTrace(); } writer.println("<html><head><title>" + title + "</title></head><body>"); //输出标题 writer.println("<h1>" + title + "</h1>"); } /** * HTML文件中的字符串 */ @Override public void makeString(String str) { writer.println("<p>" + str + "</p>"); } /** * html文件中的条目 * @param items */ @Override 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>"); } /** * 完成文档 */ @Override public void close() { writer.println("</body></html>"); writer.close(); } /** * 编写好的文档 */ public String getResult() { return filename; } }
2.6 Main类
Main类是Builder模式的测试程序。我们可以使用如下的命令来编写相应格式的文档:
java Main plain 编写纯文本文档 java Main html 编写HTML格式的文档
当我们在命令行中指定参数为plain的时候,会将TextBuilder类的实例作为参数传递至Director类的构造函数中;而若是在命令行中指定参数为html的时候,则会将HTMLBuilder类的实例作为参数传递至Director类的构造函数中。
由于TextBuilder和HTMLBuilder都是Builder的子类,因此Director仅仅使用Builder的方法即可编写文档。也就是说,Director并不关心实际编写文档的到底是TextBuilder还是HTMLBuilder。
正因为如此,我们必须在Builder中声明足够多的方法,以实现编写文档的功能,但并不包括TextBuilder和HTMLBuilder中特有的方法。
public class Main { public static void main(String[] args) { if (args.length != 1) { usage(); System.exit(0); } if (args[0].equals("plain")) { TextBuilder textBuiler = new TextBuilder(); Director director = new Director(textBuiler); director.construct(); String result = textBuiler.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文档"); } }
2.7 运行结果
运行结果:
生成的文档:
三、相关的设计模式
3.1 Template Method模板方法模式
在 Builder模式中,Director角色控制 Builder角色。在Template Method模式中,父类控制子类。
这里的“控制”指的是方法的调用顺序的控制。在Builder模式中,Director 决定了Builder角色中方法的调用顺序,而在Template Method模式中,父类决定了子类方法的调用顺序。
设计模式学习(六):Template Method模板方法模式
3.2 Composite组合模式
有些情况下 Builder模式生成的实例构成了Composite模式。
3.3 Abstract Factory抽象工厂模式
Builder模式和Abstract Factory模式都用于生成复杂的实例。
设计模式学习(九):Abstract Factory抽象工厂模式
3.4 Facade外观模式
在Builder模式中,Director角色通过组合Builder角色中的复杂方法向外部提供可以简单生成实例的接口(API)(相当于示例程序中的construct方法)。
Facade模式中的Facade角色则是通过组合内部模块向外部提供可以简单调用的接口( API )。
四、拓展思路的要点
4.1谁知道什么
在面向对象编程中,“谁知道什么”是非常重要的。也就是说,我们需要在编程时注意哪个类可以使用哪个方法以及使用这个方法到底好不好。
请大家再回忆一下示例程序。Main类并不知道(没有调用)Builder类,它只是调用了Director类的construct方法。这样,Director类就会开始工作(Main类对此一无所知),并完成文档的编写。
另一方面,Director类知道Builder类,它调用Builder类的方法来编写文档,但是它并不知道它“真正”使用的是哪个类。也就是说它并不知道它所使用的类到底是TextBuilder类、HTMLBuilder类还是其他Builder类的子类。不过也没有必要知道,因为Director类只使用了Builder类的方法,而 Builder类的子类都已经实现了那些方法。
Director类不知道自己使用的究竟是Builder类的哪个子类也好。这是因为“只有不知道子类才能替换”。不论是将TextBuilder的实例传递给Director,还是将HTMLBuilder类的实例传递给Director,它都可以正常工作,原因正是Director类不知道Builder类的具体的子类。
正是因为不知道才能够替换,正是因为可以替换,组件才具有高价值。作为设计人员,我们必须时刻关注这种“可替换性”。
4.2 设计时能够决定的事情和不能决定的事情
在Builder类中,需要声明编辑文档(实现功能)所必需的所有方法。Director类中使用的方法都是Builder类提供的。因此,在Builder类中应当定义哪些方法是非常重要的。
而且,Builder类还必须能够应对将来子类可能增加的需求。在示例程序中,我们只编写了支持纯文本文档的子类和支持HTML文件的子类。但是将来可能还会希望能够编写其他形式(例如XXXX形式)的文档。那时候,到底能不能编写出支持xxxx形式的XXXXBuilder类呢?应该不需要新的方法吧?
虽然类的设计者无法准确地预测到将来可能发生的变化,但是我们还是有必要让设计出的类能够尽可能灵活地应对近期可能发生的变化。
4.3 代码的阅读方法和修改方法
在编程时,虽然有时需要从零开始编写代码,但更多时候我们都是在现有代码的基础上进行增加和修改。这时,我们需要先阅读现有代码。不过,只是阅读抽象类的代码是无法获取很多信息的(虽然可以从方法名中获得线索)。
让我们再回顾一下示例程序。即使理解了Builder抽象类,也无法理解程序整体。至少必须在阅读了Director的代码后才能理解Builder类的使用方法(Builder类的方法的调用方法)然后再去看看TextBuilder类和HTMLBuilder类的代码,就可以明白调用Builder类的方法后具体会进行什么样的处理。
如果没有理解各个类的角色就动手增加和修改代码,在判断到底应该修改哪个类时,就会很容易出错。例如,如果修改Builder类,那么就会对Director类中调用Builder类方法的地方和Builder类的子类产生影响。或是如果不小心修改了Director类,在其内部调用了TextBuilder类的特有的方法,则会导致其失去作为可复用组件的独立性,而且当将子类替换为HTMLBuilder时,程序可能会无法正常工作。