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

所有逻辑堆砌在一个方法里,职责完全不分离。
4.2.2 建造者模式架构图(tutorials-6.0-2)
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() 输出清单

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

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

4.4 时序图
4.4.1 面条代码时序图
ShengXiangFloor DuluxCoat LevelTwoCeiling DecorationPackageController Client (ApiTest) ShengXiangFloor DuluxCoat LevelTwoCeiling DecorationPackageController Client (ApiTest) if (1 == level) 进入豪华欧式分支 手动计算价格: price += area × 0.2 × ceiling.price() price += area × 1.4 × coat.price() price += area × floor.price() 手动拼装字符串 detail getMatterList(132.52, 1) new LevelTwoCeiling() ceiling 实例 new DuluxCoat() coat 实例 new ShengXiangFloor() floor 实例 return detail (装修清单字符串)
4.4.2 建造者模式时序图
ShengXiangFloor DuluxCoat LevelTwoCeiling DecorationPackageMenu (建造者+产品) Builder (指挥者) Client (BuilderTest) ShengXiangFloor DuluxCoat LevelTwoCeiling DecorationPackageMenu (建造者+产品) Builder (指挥者) Client (BuilderTest) list.add(ceiling) price += area × 0.2 × price list.add(coat) price += area × 1.4 × price list.add(floor) price += area × 1.0 × price 遍历 list,拼装格式化清单 levelOne(132.52) new DecorationPackageMenu(132.52, "豪华欧式") new LevelTwoCeiling() ceiling 实例 appendCeiling(ceiling) return this (链式) new DuluxCoat() coat 实例 appendCoat(coat) return this (链式) new ShengXiangFloor() floor 实例 appendFloor(floor) return this (链式) return IMenu getDetail() return 装修清单字符串
每一步都有清晰的职责划分:Builder 决定"选什么",Menu 负责"怎么加"和"怎么算"。
4.5 代码分析
4.5.1 建造者模式代码(tutorials-6.0-2)
IMenu --- 建造步骤接口(规定了装修套餐可以包含哪些步骤)
java
// 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 合二为一)
java
// 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),封装预设组合方案
java
// 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)
java
// 建造者模式 ------ 简洁、语义清晰
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 硬编码)
java
// 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 × price、area × 1.4 × price 散落在每个 if 块中 |
| 高度耦合 | 方法直接依赖 7 个具体物料类,任何物料变化都可能影响此方法 |
| 可读性差 | 方法体随套餐增多急剧膨胀,难以快速理解每种套餐的组成 |
| 无法复用 | 套餐组合逻辑无法在其他场景(如导出报价单)复用 |
总结
| 维度 | 面条代码(if-else) | 建造者模式 |
|---|---|---|
| 扩展性 | 差:新增套餐需改原方法 | 好:新增 levelFour() 完全不影响已有代码 |
| 可读性 | 差:大量重复代码堆砌 | 好:链式调用如同描述套餐配置 |
| 职责划分 | 无:一个方法做所有事 | 清晰:Builder 定义方案,Menu 负责计算与展示 |
| 测试难度 | 高:修改一处可能影响全部分支 | 低:每种套餐独立构建,互不干扰 |
| 计价逻辑 | 散落各处,容易遗漏 | 统一封装在 appendXxx() 方法内 |
建造者模式适用场景总结:
- 构建对象需要多个步骤,且步骤顺序相对固定
- 需要创建同一类型但内部组成不同的多种对象(如不同档次套餐)
- 希望屏蔽复杂构建过程,让调用方只关注最终结果
- 构建过程的每一步都有独立的业务语义(如"选吊顶"、"选涂料"),适合用方法名表达意图