0%

设计模式之Builder模式

建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。

介绍

  • 意图:将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。
  • 解决问题: 主要解决在软件系统中,有时候面临着”一个复杂对象”的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。
  • 何时使用: :一些基本部件不会变,而其组合经常变化的时候。
  • 解决方法: 将变与不变分离开
    • 建造者: 创建和提供实例, 导演: 管理创造出来的实例的依赖关系.
  • 实例:
    • 一些基本部件不会变,而其组合经常变化的时候。
    • Java中的StringBuilder
  • 优点:
    • 建造者独立,易扩展
    • 便于控制细节风险
  • 缺点:
    • 产品必须有共同点,范围有限制.
    • 如内部变化复杂,会有很多的建造类
  • 使用场景
    • 需要生成的对象具有复杂的内部结构
    • 需要生成的对象内部属性本身相互依赖。
  • 注意事项
    • 与工厂模式的区别是:建造者模式更加关注与零件装配的顺序。

实现

我们假设一个快餐店的商业案例,其中,一个典型的套餐可以是一个汉堡(Burger)和一杯冷饮(Cold drink)。汉堡(Burger)可以是素食汉堡(Veg Burger)或鸡肉汉堡(Chicken Burger),它们是包在纸盒中。冷饮(Cold drink)可以是可口可乐(coke)或百事可乐(pepsi),它们是装在瓶子中。
我们将创建一个表示食物条目(比如汉堡和冷饮)的 Item 接口和实现 Item 接口的实体类,以及一个表示食物包装的 Packing 接口和实现 Packing 接口的实体类,汉堡是包在纸盒中,冷饮是装在瓶子中。
然后我们创建一个 Meal 类,带有 Item 的 ArrayList 和一个通过结合 Item 来创建不同类型的 Meal 对象的 MealBuilder。BuilderPatternDemo,我们的演示类使用 MealBuilder 来创建一个 Meal。

  • 类图
  1. 创建一个表示食物条目和食品包装的接口
    Item.java
    1
    2
    3
    4
    5
    public interface Item {
    public String name();
    public Packing packing();
    public float price();
    }
    Packing.java
    1
    2
    3
    public interface Packing {
    public String pack();
    }
  2. 创建实现Packing接口的实体类
    Wrapper.java
    1
    2
    3
    4
    5
    6
    7
    public class Wrapper implements Packing {

    @Override
    public String pack() {
    return "Wrapper";
    }
    }
    Bottle.java
    1
    2
    3
    4
    5
    6
    7
    public class Bottle implements Packing {

    @Override
    public String pack() {
    return "Bottle";
    }
    }
  3. 创建实现 Item 接口的抽象类,该类提供了默认的功能。

Burger.java

1
2
3
4
5
6
7
8
9
10
public abstract class Burger implements Item {

@Override
public Packing packing() {
return new Wrapper();
}

@Override
public abstract float price();
}

ColdDrink.java

1
2
3
4
5
6
7
8
9
10
public abstract class ColdDrink implements Item {

@Override
public Packing packing() {
return new Bottle();
}

@Override
public abstract float price();
}
  1. 创建扩展了 Burger 和 ColdDrink 的实体类。

VegBurger.java

1
2
3
4
5
6
7
8
9
10
11
12
public class VegBurger extends Burger {

@Override
public float price() {
return 25.0f;
}

@Override
public String name() {
return "Veg Burger";
}
}

ChickenBurger.java

1
2
3
4
5
6
7
8
9
10
11
12
public class ChickenBurger extends Burger {

@Override
public float price() {
return 50.5f;
}

@Override
public String name() {
return "Chicken Burger";
}
}

Coke.java

1
2
3
4
5
6
7
8
9
10
11
12
public class Coke extends ColdDrink {

@Override
public float price() {
return 30.0f;
}

@Override
public String name() {
return "Coke";
}
}

Pepsi.java

1
2
3
4
5
6
7
8
9
10
11
12
public class Pepsi extends ColdDrink {

@Override
public float price() {
return 35.0f;
}

@Override
public String name() {
return "Pepsi";
}
}
  1. 创建一个 Meal 类,带有上面定义的 Item 对象。
    Meal.java
    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
    import java.util.ArrayList;
    import java.util.List;

    public class Meal {
    private List<Item> items = new ArrayList<Item>();

    public void addItem(Item item){
    items.add(item);
    }

    public float getCost(){
    float cost = 0.0f;
    for (Item item : items) {
    cost += item.price();
    }
    return cost;
    }

    public void showItems(){
    for (Item item : items) {
    System.out.print("Item : "+item.name());
    System.out.print(", Packing : "+item.packing().pack());
    System.out.println(", Price : "+item.price());
    }
    }
    }
  2. 创建一个 MealBuilder 类,实际的 builder 类负责创建 Meal 对象。
    MealBuider.java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class MealBuilder {

    public Meal prepareVegMeal (){
    Meal meal = new Meal();
    meal.addItem(new VegBurger());
    meal.addItem(new Coke());
    return meal;
    }

    public Meal prepareNonVegMeal (){
    Meal meal = new Meal();
    meal.addItem(new ChickenBurger());
    meal.addItem(new Pepsi());
    return meal;
    }
    }
  3. BuiderPatternDemo 使用 MealBuider 来演示建造者模式(Builder Pattern)。
    BuilderPatternDemo.java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class BuilderPatternDemo {
    public static void main(String[] args) {
    MealBuilder mealBuilder = new MealBuilder();

    Meal vegMeal = mealBuilder.prepareVegMeal();
    System.out.println("Veg Meal");
    vegMeal.showItems();
    System.out.println("Total Cost: " +vegMeal.getCost());

    Meal nonVegMeal = mealBuilder.prepareNonVegMeal();
    System.out.println("\n\nNon-Veg Meal");
    nonVegMeal.showItems();
    System.out.println("Total Cost: " +nonVegMeal.getCost());
    }
    }
    再举个例子:
    类图是这样的

Builder 抽象类:
Builder.java

1
2
3
4
5
6
7
8
public abstract class Builder {

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

}

HtmlBuilder 实现类:
HtmlBuilder.java

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
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;

public class HtmlBuilder extends Builder {

private String filename;
private PrintWriter pw;
public void makeTitle(String title) {
filename="D:\\"+title+".html";
try {
pw=new PrintWriter(new FileWriter(filename));
} catch (IOException e) {
e.printStackTrace();
}
pw.println("<html><head><title>"+title+"</title></head><body>");
pw.println("<h1>"+title+"</h1>");
}

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

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

public void close() {
pw.println("</body></html>");
pw.close();
}
public String getResult(){
return filename;
}
}

TextBuilder 实现类:

TextBuilder.java

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
public class TextBuilder extends Builder {

StringBuffer sb=new StringBuffer();

public void makeTitle(String title) {
sb.append("=====================");
sb.append("["+title+"]"+"\n");
}

public void makeString(String str) {
sb.append("@"+str+"\n");
}

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

public void close() {
sb.append("=====================");
}

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

}

Director 监工类:
Director.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Director {
private Builder builder;
public Director(Builder builder){
this.builder=builder;
}
public void construct(){
String [] items1=new String[]{"奏国歌","升国旗"};
String [] items2=new String[]{"观众鼓掌","有序撤离"};
builder.makeTitle("今日头条");
builder.makeString("毕业典礼");
builder.makeItems(items1);
builder.makeString("典礼结束");
builder.makeItems(items2);
builder.close();
}
}

Main 类:
Main.java

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
public class Main {

public static void main(String[] args) {
//String choice="plain";
String choice="html";
if(choice=="plain"){
TextBuilder t=new TextBuilder();
Director d=new Director(t);
d.construct();
System.out.println(t.getResult());
}else if(choice=="html"){
HtmlBuilder html=new HtmlBuilder();
Director d=new Director(html);
d.construct();
System.out.println(html.getResult());
}else{
usage();
}

}

private static void usage() {
System.out.println("使用 plain,编辑文本文件");
System.out.println("使用 html,编辑网页文件");
}

}

Builder 模式中的登场角色

由第二个示例举例

  • Builder (建造者)
    Builder角色负责定义用于生成实例的接口(API).Builder角色中准备了用于生成实例的方法.
    示例中,Builder类扮演此角色
  • ConcreteBuilder (具体的建造者)
    ConcreteBuilder 角色是负责实现Builder角色的接口的类(API).这里定义了在生成实例时实际被调用的方法.此外,在ConcreteBuilder 角色中还定义了获取最终生成结果的方法.
    在示例程序中,由TextBuilder类和HTMLBuilder类角色扮演
  • Director (监工)
    Director角色负责使用Builder角色的接口(API)来生成示例.它并不依赖于Concrete角色.为了确保不论ConcreteBuilder角色是如何被定义的.Director都能正常工作.它只实现在Builder角色中被定义的方法.
    Director类扮演此角色
  • Client (使用者)
    该角色使用了Builder模式,Main类扮演此角色
  • Builder模式的类图
  • Builder模式的时序图

    总结

    关于Builder模式,我们一定要分清和模板方法的区别,其实就是到底谁承担了”监工”的责任,在模板方法中父类承担了这个责任,而在Builder中,有另外一个专门的类来完成这样的操作,这样做的好处是类的隔离,比如说在Main中,用户根本就不知道有Builder这个抽象类,同样的Director这个监工的根本就不管到底是哪一个实现类,因为任何一个都会被转换为父类,然后进行处理(面向抽象编程的思想),因此很好的实现了隔离,同样的这样设计的好处是复用了,隔离的越好复用起来就越方便,我们完全可以思考,假如还有另外一个监工,使用了不同的construct方法来组装这些复杂的事件,那么对于原来的代码我们不用做任何的修改,只用增加这样的一个监工类,然后定义好相应的方法就好了,之后再Main中使用,这样的一种思想使得我们不用修改源代码,复用(Builder以及其子类)就很方便了,同样的,如果想增加一个新的Builder的子类,只要照着父类的方法进行填充,再加上自己的方法就好了,完全不用修改代码,这也是一种复用,因此这种复用(组件)的思想在设计模式中随处可见,本质就是高内聚低耦合,组件开发,尽量不修改原来的代码,有可扩展性,理解了这一点,我们再看看模板方法,责任全放在了父类里,如果责任需要改变,则必须要修改父类中的责任方法了,这样就修改了原来的代码,不利于复用,这也是两者的本质区别。

拓展阅读

谁知道什么?
在面向对象编程中,”谁知道什么”是非常重要的.也就是说,我们需要在编程时注意哪个类可以使用哪个方法以及 使用这个方法到底好不好.

在实例2中,Main类并不知道(没有调用)Builder类,它只是调用了Director类的construct方法.这样Director类就会开始工作(Main类对此一无所知),并完成文档的编写.
另一方面,Director类知道Builder类,它调用Builder类的方法来编写文档,但是它并不知道它”真正”使用的是哪个类.也就是说它不知道它所使用的类到底是TextBuilder类,HTMLBuilder类还是其他Builder类的其他子类.不过也没有必要知道.因为Director类只是用了Builder类的方法,而Builder类的子类都已经实现了哪些方法.
Director类不知道自己使用的究竟是Builder类的哪个子类也好. 这是因为”只有不知道子类才能替换“,不论是将TextBuilder的实例传递给Director,还是将HTMLBuilder的实例传递给Director,它都可以正常工作,原因正是Director类还不知道Builder类的具体的子类.
正是因为不知道才能够替换,正是因为可以替换,组件才具有高价值.作为设计人员,我们必须时刻关注这种”可替换性

Thank you for your support