设计模式之建造者模式(Builder Pattern)java版本

前言

在日常软件开发中,我们经常面对这样的场景:需要组合多个零件来生成一个复杂对象,而且组合方式多种多样。如果用 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元/㎡

这些物料类分布在 ceilcoatfloortile 四个子包中,每个类都实现了 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 × pricearea × 1.4 × price 散落在每个 if 块中
高度耦合 方法直接依赖 7 个具体物料类,任何物料变化都可能影响此方法
可读性差 方法体随套餐增多急剧膨胀,难以快速理解每种套餐的组成
无法复用 套餐组合逻辑无法在其他场景(如导出报价单)复用

总结

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

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

  1. 构建对象需要多个步骤,且步骤顺序相对固定
  2. 需要创建同一类型但内部组成不同的多种对象(如不同档次套餐)
  3. 希望屏蔽复杂构建过程,让调用方只关注最终结果
  4. 构建过程的每一步都有独立的业务语义(如"选吊顶"、"选涂料"),适合用方法名表达意图
相关推荐
happymaker06262 小时前
Nexus私服的使用(配合Maven)
java·maven
JAVA学习通2 小时前
本地知识库接入大模型时的权限隔离与安全设计
java·人工智能·安全·spring
AbandonForce2 小时前
C++ 多态(多态定义 多态应用 多态底层||final override关键字||抽象类)
java·开发语言·c++
周末也要写八哥2 小时前
前端三大类设计模式学习
学习·设计模式
Bat U2 小时前
JavaEE|多线程(三)
java·前端·java-ee
卷到起飞的数分3 小时前
JVM探究
java·服务器·jvm
Geek攻城猫3 小时前
Java生产环境问题排查实战指南
java·jvm
OtIo TALL10 小时前
redis7 for windows的安装教程
java
uNke DEPH11 小时前
Spring Boot的项目结构
java·spring boot·后端