建造者模式(Builder Pattern)

前言

在日常软件开发中,我们经常面对这样的场景:需要组合多个零件来生成一个复杂对象,而且组合方式多种多样。如果用 if-else 硬编码堆砌,代码将变得又长又难维护——这正是”面条代码”的典型特征。

建造者模式(Builder Pattern)就是解决这类问题的利器。本文以装修套餐选择系统为背景,通过面条代码与建造者模式的对比,深入理解该模式的价值所在。

本文实战代码链接:codedesign2.0

参考博客:重学 Java 设计模式:实战建造者模式「各项装修物料组合套餐选配场景」 | 小傅哥 bugstack 虫洞栈


一、核心定义

建造者模式(Builder Pattern)是一种创建型设计模式,将一个复杂对象的构建过程与其表示分离,使得同样的构建过程可以创建不同的表示。

核心思想:

  • 把构建步骤标准化(接口约定),但每一步具体用什么材料可以灵活替换
  • 链式调用(Fluent Interface) 让调用侧代码高度可读
  • Director(指挥者) 封装不同预设方案,屏蔽内部组合细节

与工厂模式的区别:工厂关注”创建哪类对象”,建造者关注”如何一步步组装对象”。


二、标准体系结构图(UML)


三、场景推演:快餐店“全家桶”

想象去麦当劳或肯德基点一份“全家桶”套餐。 一个全家桶(复杂产品)通常包含:主食(汉堡/炸鸡)、饮料(可乐/果汁)、小食(薯条/鸡块)。

  • 同样的构建过程:无论你点什么套餐,服务员的准备流程都是:拿盒子 -> 装主食 -> 装小食 -> 装饮料 -> 打包。
  • 不同的表示
    • 儿童套餐(具体建造者A):迷你汉堡 + 小份薯条 + 苹果汁。
    • 巨无霸套餐(具体建造者B):巨无霸汉堡 + 大份薯条 + 大杯可乐。

在这里,服务员就是指挥者(Director),他不需要知道汉堡是怎么做的,他只需要按照固定的流程(调用Builder的方法)把东西放进托盘里。不同的套餐配方就是具体建造者(ConcreteBuilder)

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// 1. 产品类 (Product):全家桶/套餐
// 包含主食、小食和饮料等复杂部件
class Meal {
private String mainCourse; // 主食
private String snack; // 小食
private String drink; // 饮料

public void setMainCourse(String mainCourse) { this.mainCourse = mainCourse; }
public void setSnack(String snack) { this.snack = snack; }
public void setDrink(String drink) { this.drink = drink; }

@Override
public String toString() {
return "【套餐内容】: " + mainCourse + " + " + snack + " + " + drink;
}
}

// 2. 抽象建造者 (Builder):定义装配流程
// 告诉具体建造者需要实现哪些步骤
interface MealBuilder {
void buildMainCourse(); // 装主食
void buildSnack(); // 装小食
void buildDrink(); // 装饮料
Meal getMeal(); // 返回最终组合好的套餐
}

// 3. 具体建造者A (ConcreteBuilder):儿童套餐配方
class KidsMealBuilder implements MealBuilder {
private Meal meal = new Meal();

@Override
public void buildMainCourse() { meal.setMainCourse("迷你汉堡"); }
@Override
public void buildSnack() { meal.setSnack("小份薯条"); }
@Override
public void buildDrink() { meal.setDrink("苹果汁"); }
@Override
public Meal getMeal() { return meal; }
}

// 3. 具体建造者B (ConcreteBuilder):巨无霸套餐配方
class BigMacMealBuilder implements MealBuilder {
private Meal meal = new Meal();

@Override
public void buildMainCourse() { meal.setMainCourse("巨无霸汉堡"); }
@Override
public void buildSnack() { meal.setSnack("大份薯条"); }
@Override
public void buildDrink() { meal.setDrink("大杯可乐"); }
@Override
public Meal getMeal() { return meal; }
}

// 4. 指挥者 (Director):服务员
// 服务员不关心汉堡怎么做,只负责按照标准流程进行组装
class Waiter {
public Meal construct(MealBuilder builder) {
System.out.println("服务员开始准备套餐 (拿盒子)...");
// 按照固定且标准的流程组装
builder.buildMainCourse();
builder.buildSnack();
builder.buildDrink();
System.out.println("打包完成!");
return builder.getMeal();
}
}

// 5. 客户端测试代码 (Client):顾客点餐
public class FastFoodClient {
public static void main(String[] args) {
Waiter waiter = new Waiter(); // 召唤指挥者:服务员

// --- 场景 1:顾客点儿童套餐 ---
System.out.println("--- 顾客A点儿童套餐 ---");
MealBuilder kidsBuilder = new KidsMealBuilder();
Meal kidsMeal = waiter.construct(kidsBuilder);
System.out.println("交付产品: " + kidsMeal);
System.out.println();

// --- 场景 2:顾客点巨无霸套餐 ---
System.out.println("--- 顾客B点巨无霸套餐 ---");
MealBuilder bigMacBuilder = new BigMacMealBuilder();
Meal bigMacMeal = waiter.construct(bigMacBuilder);
System.out.println("交付产品: " + bigMacMeal);
}
}

四、实战案例:装修套餐选择系统

4.1 需求分析

4.1.1 业务背景

装修公司提供三种标准化套餐,每种套餐由吊顶涂料、**地面(地板/地砖)**三类物料组合而成:

套餐等级 名称 吊顶 涂料 地面
Level 1 豪华欧式 二级顶(¥850/㎡) 多乐士 Dulux(¥719/㎡) 圣象地板(¥318/㎡)
Level 2 轻奢田园 二级顶(¥850/㎡) 立邦(¥650/㎡) 马可波罗地砖(¥140/㎡)
Level 3 现代简约 一级顶(¥260/㎡) 立邦(¥650/㎡) 东鹏地砖(¥102/㎡)

4.1.2 计价规则

$$\text{吊顶费用} = \text{房屋面积} \times 0.2 \times \text{吊顶单价}$$

$$\text{涂料费用} = \text{房屋面积} \times 1.4 \times \text{涂料单价}$$

$$\text{地面费用} = \text{房屋面积} \times \text{地面单价}$$

吊顶系数 0.2 表示吊顶实际铺设面积约占总面积的 20%;涂料系数 1.4 表示四面墙体涂刷面积约为地面面积的 140%。

4.1.3 所有物料汇总(tutorials-6.0-0)

Matter 接口定义了所有装修物料的统一契约:

1
2
3
4
5
6
7
public interface Matter {
String scene(); // 应用场景:吊顶/涂料/地板/地砖
String brand(); // 品牌名称
String model(); // 型号规格
BigDecimal price(); // 平米报价
String desc(); // 品牌描述
}

物料清单一览

分类 类名 品牌 型号 平米价格
吊顶 LevelOneCeiling 装修公司自带 一级顶 260 元
吊顶 LevelTwoCeiling 装修公司自带 二级顶 850 元
涂料 DuluxCoat 多乐士(Dulux) 第二代 719 元
涂料 LiBangCoat 立邦 默认级别 650 元
地板 DerFloor 德尔(Der) A+ 119 元
地板 ShengXiangFloor 圣象 一级 318 元
地砖 DongPengTile 东鹏瓷砖 10001 102 元
地砖 MarcoPoloTile 马可波罗 缺省 140 元

物料继承关系图

这些物料类分布在 ceilcoatfloortile 四个子包中,每个类都实现了 Matter 接口的 5 个方法,提供了各自的品牌、型号、价格等信息。


4.2 架构图

4.2.1 面条代码架构图(tutorials-6.0-1)

1
2
3
4
5
6
ApiTest
└── DecorationPackageController
└── getMatterList(area, level)
├── if (level == 1) { 直接 new 各物料,手动累加价格 }
├── if (level == 2) { 直接 new 各物料,手动累加价格 }
└── if (level == 3) { 直接 new 各物料,手动累加价格 }

image-20260422110131510

所有逻辑堆砌在一个方法里,职责完全不分离。

4.2.2 建造者模式架构图(tutorials-6.0-2)

1
2
3
4
5
6
7
8
9
10
11
12
ApiTest
└── Builder(指挥者/Director)
├── levelOne(area) ──────────────────────────────────┐
├── levelTwo(area) ──────────────────────────────────┤
└── levelThree(area) ──────────────────────────────────┤

new DecorationPackageMenu(area, grade)
.appendCeiling(matter) ← 返回 IMenu
.appendCoat(matter) ← 返回 IMenu
.appendFloor/Tile(matter)← 返回 IMenu

ApiTest 拿到 IMenu → 调用 .getDetail() 输出清单

image-20260422111511378

职责清晰:Builder 决定”用什么组合”,DecorationPackageMenu 负责”怎么计算和展示”。


4.3 类图对比

4.3.1 面条代码类图

image-20260422140042816

问题:Controller 直接依赖 7 个具体实现类,高度耦合,新增套餐必须修改此类。

4.3.2 建造者模式类图

image-20260422142234998


4.4 时序图

4.4.1 面条代码时序图

4.4.2 建造者模式时序图

每一步都有清晰的职责划分:Builder 决定”选什么”,Menu 负责”怎么加”和”怎么算”


4.5 代码分析

4.5.1 建造者模式代码(tutorials-6.0-2)

IMenu — 建造步骤接口(规定了装修套餐可以包含哪些步骤)

1
2
3
4
5
6
7
8
// tutorials-6.0-2: IMenu.java
public interface IMenu {
IMenu appendCeiling(Matter matter); // 返回 IMenu,支持链式调用
IMenu appendCoat(Matter matter);
IMenu appendFloor(Matter matter);
IMenu appendTile(Matter matter);
String getDetail(); // 最终构建产物:清单字符串
}

每个 append 方法都返回 IMenu 自身,这正是**链式调用(Fluent Interface)**的关键——使调用代码像自然语言一样流畅。


DecorationPackageMenu — 具体建造者(Product + ConcreteBuilder 合二为一)

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
40
41
42
43
// tutorials-6.0-2: DecorationPackageMenu.java
public class DecorationPackageMenu implements IMenu {

private List<Matter> list = new ArrayList<>();
private BigDecimal price = BigDecimal.ZERO;
private BigDecimal area;
private String grade;

private DecorationPackageMenu() {} // 禁止无参构建,强制传入面积和等级名

public DecorationPackageMenu(Double area, String grade) {
this.area = new BigDecimal(area);
this.grade = grade;
}

public IMenu appendCeiling(Matter matter) {
list.add(matter);
// 吊顶:面积 × 0.2 × 单价
price = price.add(area.multiply(new BigDecimal("0.2")).multiply(matter.price()));
return this; // 链式调用核心
}

public IMenu appendCoat(Matter matter) {
list.add(matter);
// 涂料:面积 × 1.4 × 单价
price = price.add(area.multiply(new BigDecimal("1.4")).multiply(matter.price()));
return this;
}

public IMenu appendFloor(Matter matter) {
list.add(matter);
price = price.add(area.multiply(matter.price())); // 地面:直接乘单价
return this;
}

public IMenu appendTile(Matter matter) {
list.add(matter);
price = price.add(area.multiply(matter.price()));
return this;
}

public String getDetail() { /* 格式化输出清单 */ }
}

关键设计点:

  • 私有无参构造:强制调用方必须提供面积和套餐名,避免漏填
  • 每步独立计算:吊顶/涂料/地面的计价系数各自封装在对应方法中,互不干扰
  • return this:使链式调用成为可能,调用侧十分简洁

Builder — 指挥者(Director),封装预设组合方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// tutorials-6.0-2: Builder.java
public class Builder {

public IMenu levelOne(Double area) {
return new DecorationPackageMenu(area, "豪华欧式")
.appendCeiling(new LevelTwoCeiling()) // 二级顶
.appendCoat(new DuluxCoat()) // 多乐士
.appendFloor(new ShengXiangFloor()); // 圣象地板
}

public IMenu levelTwo(Double area) {
return new DecorationPackageMenu(area, "轻奢田园")
.appendCeiling(new LevelTwoCeiling()) // 二级顶
.appendCoat(new LiBangCoat()) // 立邦
.appendTile(new MarcoPoloTile()); // 马可波罗
}

public IMenu levelThree(Double area) {
return new DecorationPackageMenu(area, "现代简约")
.appendCeiling(new LevelOneCeiling()) // 一级顶
.appendCoat(new LiBangCoat()) // 立邦
.appendTile(new DongPengTile()); // 东鹏
}
}

调用方(ApiTest)只需 builder.levelOne(132.52D).getDetail()完全不需要知道套餐由哪些物料构成,细节全部被 Builder 封装。


调用侧对比(ApiTest)

1
2
3
4
5
// 建造者模式 —— 简洁、语义清晰
Builder builder = new Builder();
System.out.println(builder.levelOne(132.52D).getDetail());
System.out.println(builder.levelTwo(98.25D).getDetail());
System.out.println(builder.levelThree(85.43D).getDetail());

4.5.2 面条代码(if-else 硬编码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// tutorials-6.0-1: DecorationPackageController.java
public String getMatterList(BigDecimal area, Integer level) {
List<Matter> list = new ArrayList<>();
BigDecimal price = BigDecimal.ZERO;

if (1 == level) { // ← 硬编码条件
LevelTwoCeiling levelTwoCeiling = new LevelTwoCeiling();
DuluxCoat duluxCoat = new DuluxCoat();
ShengXiangFloor shengXiangFloor = new ShengXiangFloor();
list.add(levelTwoCeiling);
// ...
price = price.add(area.multiply(new BigDecimal("0.2")).multiply(levelTwoCeiling.price())); // 计价逻辑重复
price = price.add(area.multiply(new BigDecimal("1.4")).multiply(duluxCoat.price())); // 计价逻辑重复
price = price.add(area.multiply(shengXiangFloor.price()));
}
if (2 == level) { /* 相同结构再写一遍 */ }
if (3 == level) { /* 相同结构再写一遍 */ }
// ...
}

面条代码问题清单:

问题 说明
违反开闭原则 新增套餐级别必须修改此方法,加一个 if
计价逻辑重复 area × 0.2 × pricearea × 1.4 × price 散落在每个 if 块中
高度耦合 方法直接依赖 7 个具体物料类,任何物料变化都可能影响此方法
可读性差 方法体随套餐增多急剧膨胀,难以快速理解每种套餐的组成
无法复用 套餐组合逻辑无法在其他场景(如导出报价单)复用

总结

维度 面条代码(if-else) 建造者模式
扩展性 差:新增套餐需改原方法 好:新增 levelFour() 完全不影响已有代码
可读性 差:大量重复代码堆砌 好:链式调用如同描述套餐配置
职责划分 无:一个方法做所有事 清晰:Builder 定义方案,Menu 负责计算与展示
测试难度 高:修改一处可能影响全部分支 低:每种套餐独立构建,互不干扰
计价逻辑 散落各处,容易遗漏 统一封装在 appendXxx() 方法内

建造者模式适用场景总结:

  1. 构建对象需要多个步骤,且步骤顺序相对固定
  2. 需要创建同一类型但内部组成不同的多种对象(如不同档次套餐)
  3. 希望屏蔽复杂构建过程,让调用方只关注最终结果
  4. 构建过程的每一步都有独立的业务语义(如”选吊顶”、”选涂料”),适合用方法名表达意图