建造者模式(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。
- 类图
- 创建一个表示食物条目和食品包装的接口
Item.javaPacking.java1
2
3
4
5public interface Item {
public String name();
public Packing packing();
public float price();
}1
2
3public interface Packing {
public String pack();
} - 创建实现Packing接口的实体类
Wrapper.javaBottle.java1
2
3
4
5
6
7public class Wrapper implements Packing {
public String pack() {
return "Wrapper";
}
}1
2
3
4
5
6
7public class Bottle implements Packing {
public String pack() {
return "Bottle";
}
} - 创建实现 Item 接口的抽象类,该类提供了默认的功能。
Burger.java
1 | public abstract class Burger implements Item { |
ColdDrink.java
1 | public abstract class ColdDrink implements Item { |
- 创建扩展了 Burger 和 ColdDrink 的实体类。
VegBurger.java
1 | public class VegBurger extends Burger { |
ChickenBurger.java
1 | public class ChickenBurger extends Burger { |
Coke.java
1 | public class Coke extends ColdDrink { |
Pepsi.java
1 | public class Pepsi extends ColdDrink { |
- 创建一个 Meal 类,带有上面定义的 Item 对象。
Meal.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26import 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());
}
}
} - 创建一个 MealBuilder 类,实际的 builder 类负责创建 Meal 对象。
MealBuider.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public 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;
}
} - BuiderPatternDemo 使用 MealBuider 来演示建造者模式(Builder Pattern)。
BuilderPatternDemo.java再举个例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public 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 | public abstract class Builder { |
HtmlBuilder 实现类:
HtmlBuilder.java
1 | import java.io.FileWriter; |
TextBuilder 实现类:
TextBuilder.java
1 | public class TextBuilder extends Builder { |
Director 监工类:
Director.java
1 | public class Director { |
Main 类:
Main.java
1 | public class Main { |
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类的具体的子类.
正是因为不知道才能够替换,正是因为可以替换,组件才具有高价值.作为设计人员,我们必须时刻关注这种”可替换性“