🔥 核心
通过建造器,使用多个简单的对象一步一步构造出一个复杂的对象。
🙁 问题场景
你现在从一名程序开发者转行为了一名房屋建筑师。你的任务就是建房子。
你很快建好了一个 房子(House)
。这个房子普普通通,压根不是什么难事儿。很快,你收到了越来越多的订单,房子好像愈加复杂了起来:
一个 带花园的房子(HouseWithGarden)
…
一个 带车库的房子(HouseWithGarage)
…
一个 带泳池的房子(HouseWithSwimmingPool)
…
一个 带火箭基地的的房子(HouseWithRocketBase)
…
一个 既带花园又带泳池的房子(HouseWithGardenAndSwimmingPool)
…
一个 既带车库又带泳池又带火箭基地的房子(HouseWithGardenAndSwimmingPoolAndRocketBase)
…
于是,你以 房子
为基类 ,派生出所有各种各样的房子的子类…等等,房子的种类实在是太多了,为每一种房子创建一个子类显然是愚蠢的行为。
你立即发现了所谓房子的本质——无非是 房子本身
、花园
、车库
、泳池
、火箭基地
的组合。所以,你可以使用一个包含所有可能参数的超级构造函数。使用超级构造函数后,的确避免了生成子类,但是这使得对于构造函数的调用十分不简洁:
new House(3, 2, 4, true, false, true, true, true, false, false, true, false, false, true, true, false);
还有什么其他的注意吗?
🙂 解决方案
你突然意识到,对于这种复杂对象的构造,是不可能一步到位的。
也就是说,必须使用多个简单的对象一步一步构造出一个复杂的对象。复杂对象的整个构造过程,被拆分为一个个小步骤,然后在建造器中调用一个个小步骤。
很快,你写好了以下的小步骤:
建造花园;
建造车库;
建造泳池;
建造火箭基地…
对于 既带花园又带泳池的房子
,就可以调用 建造花园 + 建造泳池。
对于 既带车库又带泳池又带火箭基地的房子
,就可以调用 建造车库 + 建造车库 + 建造火箭基地。
对于一种房子,你仅仅需要调用它需要的一组步骤即可!
🌈 有趣的例子
建造者模式随处可见。在餐厅里,有许多食物(蛋糕(Cake)
、牛肉(Beef)
)和饮品(咖啡(Coffee)
、红酒(Redwine)
)。它们都继承了 单品(Item)
接口。
餐厅提供了多种 套餐(Meal)
。使用建造器,就能组合出这些套餐:
1)蛋糕
+ 咖啡
2)牛肉
+ 红酒
单品接口
interface Item {
String getName();
int getPrice();
}
蛋糕类
class Cake implements Item {
@Override
public String getName() {
return "我是一块蛋糕";
}
@Override
public int getPrice() {
return 12;
}
}
牛肉类
class Beef implements Item {
@Override
public String getName() {
return "我是一份牛肉";
}
@Override
public int getPrice() {
return 80;
}
}
咖啡类
class Coffee implements Item {
@Override
public String getName() {
return "我是一杯咖啡";
}
@Override
public int getPrice() {
return 30;
}
}
红酒类
class RedWine implements Item {
@Override
public String getName() {
return "我是一瓶红酒";
}
@Override
public int getPrice() {
return 400;
}
}
一顿正餐
通常是单品组合成的套餐
class Meal {
private List<Item> items = new ArrayList<>();
public void addItem(Item item) {
items.add(item);
}
public void showItems() {
for (Item item : items) {
System.out.println(item.getName());
}
}
public void showTotalPrice() {
int total = 0;
for (Item item : items) {
total += item.getPrice();
}
System.out.println(total);
}
}
套餐建造器
class MealBuilder {
public Meal prepareMeal1() {
Meal meal = new Meal();
meal.addItem(new Cake());
meal.addItem(new Coffee());
return meal;
}
public Meal prepareMeal2() {
Meal meal = new Meal();
meal.addItem(new Beef());
meal.addItem(new RedWine());
return meal;
}
}
public class BuilderPatternDemo {
public static void main(String[] args) {
// 新建一个正餐建造器
MealBuilder mealBuilder = new MealBuilder();
// 构造套餐1
Meal meal1 = mealBuilder.prepareMeal1();
// 展示一下套餐
meal1.showItems();
meal1.showTotalPrice();
// 构造套餐2
Meal meal2 = mealBuilder.prepareMeal2();
// 展示一下套餐
meal2.showItems();
meal2.showTotalPrice();
}
}
我是一块蛋糕
我是一杯咖啡
42
我是一份牛肉
我是一瓶红酒
480
☘️ 使用场景
◾️使用建造器模式可避免 “重叠构造函数 (telescopic constructor)” 的出现。
假设你的构造函数中有十个可选参数,那么调用该函数会非常不方便;因此,你需要重载这个构造函数,新建几个只有较少参数的简化版。但这些构造函数仍需调用主构造函数,传递一些默认数值来替代省略掉的参数。
class Pizza {
Pizza(int size) { … }
Pizza(int size, boolean cheese) { … }
Pizza(int size, boolean cheese, boolean pepperoni) { … }
// …
只有在 C# 或 Java 等支持方法重载的编程语言中才能写出如此复杂的构造函数。
建造器模式让你可以分步骤构造对象,而且允许你仅使用必须的步骤。应用该模式后,你再也不需要将几十个参数塞进构造函数里了。
◾️当你希望使用代码创建不同形式的产品(例如石头或木头房屋)时,可使用建造器模式。
如果你需要创建的各种形式的产品,它们的制造过程相似且仅有细节上的差异,此时可使用建造器模式。
基本建造器接口中定义了所有可能的制造步骤,具体建造器将实现这些步骤来制造特定形式的产品。同时,主管类将负责管理制造步骤的顺序。
◾️使用建造器构造组合树或其他复杂对象。
建造器模式让你能分步骤构造产品。你可以延迟执行某些步骤而不会影响最终产品。你甚至可以递归调用这些步骤,这在创建对象树时非常方便。
建造器在执行制造步骤时,不能对外发布未完成的产品。这可以避免客户端代码获取到不完整结果对象的情况。
🧊 实现方式
(1)清晰地定义通用步骤,确保它们可以制造所有形式的产品。否则你将无法进一步实施该模式。
(2)在基本建造器接口中声明这些步骤。
(3)为每个形式的产品创建具体建造器类,并实现其构造步骤。
(4)考虑创建主管类。它可以使用同一建造器对象来封装多种构造产品的方式。
(5)客户端代码会同时创建建造器和主管对象。构造开始前,客户端必须将建造器对象传递给主管对象。通常情况下,客户端只需调用主管类构造函数一次即可。主管类使用建造器对象完成后续所有制造任务。还有另一种方式,那就是客户端可以将建造器对象直接传递给主管类的制造方法。
(6)只有在所有产品都遵循相同接口的情况下,构造结果可以直接通过主管类获取。否则,客户端应当通过建造器获取构造结果。
🎲 优缺点
➕ 你可以分步创建对象,暂缓创建步骤或递归运行创建步骤。
➕ 生成不同形式的产品时,你可以复用相同的构造代码。
➕ 单一职责原则。你可以将复杂构造代码从产品的业务逻辑中分离出来。
➖ 产品必须有共同点,从而有相同的构造子步骤。
➖ 如果内部变化复杂,会有很多的建造类。
➖ 由于该模式需要新增多个类,因此代码整体复杂程度会有所增加。
🌸 补充
上面提到了一个没有详细讲解的概念:主管类(Director)。
主管类位于建造器和调用者之间,用于对建造器再进行一次复杂的组合;同时,它也使得建造器的细节对调用者完全隐藏。另外,主管类不是必须的。