设计模式之建造者模式(Builder Pattern)
建造者模式(Builder Pattern)
前言
在日常软件开发中,我们经常面对这样的场景:需要组合多个零件来生成一个复杂对象,而且组合方式多种多样。如果用 if-else 硬编码堆砌,代码将变得又长又难维护——这正是”面条代码”的典型特征。
建造者模式(Builder Pattern)就是解决这类问题的利器。本文以装修套餐选择系统为背景,通过面条代码与建造者模式的对比,深入理解该模式的价值所在。
本文实战代码链接:codedesign2.0
参考博客:重学 Java 设计模式:实战建造者模式「各项装修物料组合套餐选配场景」 | 小傅哥 bugstack 虫洞栈
一、核心定义
建造者模式(Builder Pattern)是一种创建型设计模式,将一个复杂对象的构建过程与其表示分离,使得同样的构建过程可以创建不同的表示。
核心思想:
- 把构建步骤标准化(接口约定),但每一步具体用什么材料可以灵活替换
- 链式调用(Fluent Interface) 让调用侧代码高度可读
- Director(指挥者) 封装不同预设方案,屏蔽内部组合细节
与工厂模式的区别:工厂关注”创建哪类对象”,建造者关注”如何一步步组装对象”。
二、标准体系结构图(UML)
classDiagram
class Director {
-Builder builder
+construct()
}
class Builder {
<<interface>>
+buildPartA()
+buildPartB()
+buildPartC()
+getResult() Product
}
class ConcreteBuilder {
-Product product
+buildPartA()
+buildPartB()
+buildPartC()
+getResult() Product
}
class Product {
-String partA
-String partB
-String partC
}
Director o--> Builder
ConcreteBuilder ..|> Builder
ConcreteBuilder ..> Product : creates
三、场景推演:快餐店“全家桶”
想象去麦当劳或肯德基点一份“全家桶”套餐。 一个全家桶(复杂产品)通常包含:主食(汉堡/炸鸡)、饮料(可乐/果汁)、小食(薯条/鸡块)。
- 同样的构建过程:无论你点什么套餐,服务员的准备流程都是:拿盒子 -> 装主食 -> 装小食 -> 装饮料 -> 打包。
- 不同的表示:
- 儿童套餐(具体建造者A):迷你汉堡 + 小份薯条 + 苹果汁。
- 巨无霸套餐(具体建造者B):巨无霸汉堡 + 大份薯条 + 大杯可乐。
在这里,服务员就是指挥者(Director),他不需要知道汉堡是怎么做的,他只需要按照固定的流程(调用Builder的方法)把东西放进托盘里。不同的套餐配方就是具体建造者(ConcreteBuilder)。
1 | // 1. 产品类 (Product):全家桶/套餐 |
四、实战案例:装修套餐选择系统
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 | public interface Matter { |
物料清单一览:
| 分类 | 类名 | 品牌 | 型号 | 平米价格 |
|---|---|---|---|---|
| 吊顶 | LevelOneCeiling |
装修公司自带 | 一级顶 | 260 元 |
| 吊顶 | LevelTwoCeiling |
装修公司自带 | 二级顶 | 850 元 |
| 涂料 | DuluxCoat |
多乐士(Dulux) | 第二代 | 719 元 |
| 涂料 | LiBangCoat |
立邦 | 默认级别 | 650 元 |
| 地板 | DerFloor |
德尔(Der) | A+ | 119 元 |
| 地板 | ShengXiangFloor |
圣象 | 一级 | 318 元 |
| 地砖 | DongPengTile |
东鹏瓷砖 | 10001 | 102 元 |
| 地砖 | MarcoPoloTile |
马可波罗 | 缺省 | 140 元 |
物料继承关系图:
classDiagram
class Matter {
<<interface>>
+scene() String
+brand() String
+model() String
+price() BigDecimal
+desc() String
}
class LevelOneCeiling {
一级顶 · 260元/㎡
}
class LevelTwoCeiling {
二级顶 · 850元/㎡
}
class DuluxCoat {
多乐士 · 719元/㎡
}
class LiBangCoat {
立邦 · 650元/㎡
}
class DerFloor {
德尔 · 119元/㎡
}
class ShengXiangFloor {
圣象 · 318元/㎡
}
class DongPengTile {
东鹏 · 102元/㎡
}
class MarcoPoloTile {
马可波罗 · 140元/㎡
}
Matter <|.. LevelOneCeiling
Matter <|.. LevelTwoCeiling
Matter <|.. DuluxCoat
Matter <|.. LiBangCoat
Matter <|.. DerFloor
Matter <|.. ShengXiangFloor
Matter <|.. DongPengTile
Matter <|.. MarcoPoloTile
这些物料类分布在 ceil、coat、floor、tile 四个子包中,每个类都实现了 Matter 接口的 5 个方法,提供了各自的品牌、型号、价格等信息。
4.2 架构图
4.2.1 面条代码架构图(tutorials-6.0-1)
1 | ApiTest |

所有逻辑堆砌在一个方法里,职责完全不分离。
4.2.2 建造者模式架构图(tutorials-6.0-2)
1 | ApiTest |

职责清晰:Builder 决定”用什么组合”,DecorationPackageMenu 负责”怎么计算和展示”。
4.3 类图对比
4.3.1 面条代码类图

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

4.4 时序图
4.4.1 面条代码时序图
sequenceDiagram
participant Client as Client (ApiTest)
participant Controller as DecorationPackageController
participant Ceiling as LevelTwoCeiling
participant Coat as DuluxCoat
participant Floor as ShengXiangFloor
Client->>Controller: getMatterList(132.52, 1)
Note over Controller: if (1 == level) 进入豪华欧式分支
Controller->>Ceiling: new LevelTwoCeiling()
Ceiling-->>Controller: ceiling 实例
Controller->>Coat: new DuluxCoat()
Coat-->>Controller: coat 实例
Controller->>Floor: new ShengXiangFloor()
Floor-->>Controller: floor 实例
Note over Controller: 手动计算价格:<br/>price += area × 0.2 × ceiling.price()<br/>price += area × 1.4 × coat.price()<br/>price += area × floor.price()
Note over Controller: 手动拼装字符串 detail
Controller-->>Client: return detail (装修清单字符串)
4.4.2 建造者模式时序图
sequenceDiagram
participant Client as Client (BuilderTest)
participant B as Builder (指挥者)
participant Menu as DecorationPackageMenu (建造者+产品)
participant C as LevelTwoCeiling
participant Co as DuluxCoat
participant F as ShengXiangFloor
Client->>B: levelOne(132.52)
B->>Menu: new DecorationPackageMenu(132.52, "豪华欧式")
activate Menu
B->>C: new LevelTwoCeiling()
C-->>B: ceiling 实例
B->>Menu: appendCeiling(ceiling)
Note over Menu: list.add(ceiling)<br/>price += area × 0.2 × price
Menu-->>B: return this (链式)
B->>Co: new DuluxCoat()
Co-->>B: coat 实例
B->>Menu: appendCoat(coat)
Note over Menu: list.add(coat)<br/>price += area × 1.4 × price
Menu-->>B: return this (链式)
B->>F: new ShengXiangFloor()
F-->>B: floor 实例
B->>Menu: appendFloor(floor)
Note over Menu: list.add(floor)<br/>price += area × 1.0 × price
Menu-->>B: return this (链式)
deactivate Menu
B-->>Client: return IMenu
Client->>Menu: getDetail()
Note over Menu: 遍历 list,拼装格式化清单
Menu-->>Client: return 装修清单字符串
每一步都有清晰的职责划分:Builder 决定”选什么”,Menu 负责”怎么加”和”怎么算”。
4.5 代码分析
4.5.1 建造者模式代码(tutorials-6.0-2)
IMenu — 建造步骤接口(规定了装修套餐可以包含哪些步骤)
1 | // tutorials-6.0-2: IMenu.java |
每个
append方法都返回IMenu自身,这正是**链式调用(Fluent Interface)**的关键——使调用代码像自然语言一样流畅。
DecorationPackageMenu — 具体建造者(Product + ConcreteBuilder 合二为一)
1 | // tutorials-6.0-2: DecorationPackageMenu.java |
关键设计点:
- 私有无参构造:强制调用方必须提供面积和套餐名,避免漏填
- 每步独立计算:吊顶/涂料/地面的计价系数各自封装在对应方法中,互不干扰
return this:使链式调用成为可能,调用侧十分简洁
Builder — 指挥者(Director),封装预设组合方案
1 | // tutorials-6.0-2: Builder.java |
调用方(
ApiTest)只需builder.levelOne(132.52D).getDetail(),完全不需要知道套餐由哪些物料构成,细节全部被Builder封装。
调用侧对比(ApiTest)
1 | // 建造者模式 —— 简洁、语义清晰 |
4.5.2 面条代码(if-else 硬编码)
1 | // tutorials-6.0-1: DecorationPackageController.java |
面条代码问题清单:
| 问题 | 说明 |
|---|---|
| 违反开闭原则 | 新增套餐级别必须修改此方法,加一个 if 块 |
| 计价逻辑重复 | area × 0.2 × price、area × 1.4 × price 散落在每个 if 块中 |
| 高度耦合 | 方法直接依赖 7 个具体物料类,任何物料变化都可能影响此方法 |
| 可读性差 | 方法体随套餐增多急剧膨胀,难以快速理解每种套餐的组成 |
| 无法复用 | 套餐组合逻辑无法在其他场景(如导出报价单)复用 |
总结
| 维度 | 面条代码(if-else) | 建造者模式 |
|---|---|---|
| 扩展性 | 差:新增套餐需改原方法 | 好:新增 levelFour() 完全不影响已有代码 |
| 可读性 | 差:大量重复代码堆砌 | 好:链式调用如同描述套餐配置 |
| 职责划分 | 无:一个方法做所有事 | 清晰:Builder 定义方案,Menu 负责计算与展示 |
| 测试难度 | 高:修改一处可能影响全部分支 | 低:每种套餐独立构建,互不干扰 |
| 计价逻辑 | 散落各处,容易遗漏 | 统一封装在 appendXxx() 方法内 |
建造者模式适用场景总结:
- 构建对象需要多个步骤,且步骤顺序相对固定
- 需要创建同一类型但内部组成不同的多种对象(如不同档次套餐)
- 希望屏蔽复杂构建过程,让调用方只关注最终结果
- 构建过程的每一步都有独立的业务语义(如”选吊顶”、”选涂料”),适合用方法名表达意图