GoF设计模式——建造者模式

本文是【GoF设计模式】系列第4篇,更多内容欢迎关注公众号:咖啡八杯

前言

为什么需要建造者模式?

GoF设计模式------抽象工厂模式 中,抽象工厂解决了"一族产品要风格统一"的问题------一个工厂负责一整套产品,选了工厂就等于选了整套风格。

但不管是工厂方法还是抽象工厂,都只管"产出什么",不管"怎么一步步造出来"。有些对象的构造过程很复杂------不是一句 new 就能搞定的:

java 复制代码
// 构造一台电脑:要装 CPU、内存、硬盘,顺序还有讲究
Computer computer = new Computer("Intel i9-13900K", "DDR5 64GB", "NVMe SSD 2TB");
// 问题1:参数多了根本分不清哪个是哪个
// 问题2:有些参数可选,构造函数需要大量重载
// 问题3:构造逻辑散在业务代码中,换个产品就要重写

当对象构造变复杂时,直接用构造函数有三个痛点:

  • 参数爆炸:十几个参数的构造函数难以阅读,调用方容易传错位置
  • 参数可选:有些必填、有些选填,构造函数需要大量重载来覆盖各种组合
  • 构建逻辑分散:构造细节混在业务代码中,换种产品就得重写

建造者模式把这些细节封装到 Builder 里,调用方不用管里面怎么造。

概念

建造者模式(Builder Pattern)是创建型 设计模式,核心思想:将复杂对象的构建过程与表示分离,用相同的构建步骤创建不同的产品。

建造者模式包含四个核心角色:

  • Product(产品) :要构建的复杂对象
  • Builder(抽象建造者) :定义构建步骤的接口,规定"必须做哪几步"
  • ConcreteBuilder(具体建造者) :实现每个步骤,每种产品变体一个
  • Director(指导者) :编排构建顺序,封装"先做什么、后做什么"
classDiagram direction BT class Director { -Builder builder +Director(Builder builder) +construct() Product } class Builder { <> +buildPartA() void +buildPartB() void +getResult() Product } class ConcreteBuilder1 { -Product product +buildPartA() void +buildPartB() void +getResult() Product } class ConcreteBuilder2 { -Product product +buildPartA() void +buildPartB() void +getResult() Product } class Product { -partA String -partB String } Director --> Builder : 使用 ConcreteBuilder1 ..|> Builder : 实现 ConcreteBuilder2 ..|> Builder : 实现 ConcreteBuilder1 ..> Product : 创建 ConcreteBuilder2 ..> Product : 创建

分工逻辑:Builder 定步骤 → ConcreteBuilder 实现步骤 → Director 排顺序 → Product 出结果。

要点 :建造者模式解决的是构建过程与表示分离。Director 封装"怎么造"的算法,Builder 决定"造出来长什么样"。同一个 Director 搭配不同 Builder,构建算法不变,但产出不同产品。

实现

GoF 标准实现:四角色模式

GoF 原版,包含完整 Director。适合构建算法固定、产品变体多的场景。

java 复制代码
// 产品
public class Product {
	private String partA;
	private String partB;

	public void setPartA(String partA) { this.partA = partA; }
	public void setPartB(String partB) { this.partB = partB; }

	@Override
	public String toString() {
		return "Product{partA='" + partA + "', partB='" + partB + "'}";
	}
}

// 抽象建造者:定义构建步骤
public interface Builder {
	public void buildPartA();
	public void buildPartB();
	public Product getResult();
}

// 具体建造者 1
public class ConcreteBuilder1 implements Builder {
	private Product product = new Product();

	@Override
	public void buildPartA() { product.setPartA("PartA-Type1"); }

	@Override
	public void buildPartB() { product.setPartB("PartB-Type1"); }

	@Override
	public Product getResult() { return product; }
}

// 具体建造者 2
public class ConcreteBuilder2 implements Builder {
	private Product product = new Product();

	@Override
	public void buildPartA() { product.setPartA("PartA-Type2"); }

	@Override
	public void buildPartB() { product.setPartB("PartB-Type2"); }

	@Override
	public Product getResult() { return product; }
}

// 指导者:封装构建算法(步骤顺序)
public class Director {
	private Builder builder;

	public Director(Builder builder) {
		this.builder = builder;
	}

	public Product construct() {
		builder.buildPartA();  // 第一步
		builder.buildPartB();  // 第二步
		return builder.getResult();
	}
}

// 客户端:同一个 Director,换 Builder 出不同产品
Builder builder1 = new ConcreteBuilder1();
Director director = new Director(builder1);
Product product1 = director.construct();

Builder builder2 = new ConcreteBuilder2();
director = new Director(builder2);
Product product2 = director.construct();

核心价值:Director 封装了固定的构建流程,换一个 Builder 就能产出不同配置,构建算法完全不变。

简化实现:链式调用(Fluent Builder)

省略 Director,Builder 自身通过 return this 支持链式调用。这是实际开发中最常用的版本,适合构建过程简单的场景------不需要严格的步骤顺序,只是字段赋值。

java 复制代码
public class User {
	private final String name;      // 必填
	private final int age;          // 必填
	private final String email;     // 选填
	private final String phone;     // 选填
	private final String address;   // 选填

	private User(Builder builder) {
		this.name = builder.name;
		this.age = builder.age;
		this.email = builder.email;
		this.phone = builder.phone;
		this.address = builder.address;
	}

	public static class Builder {
		private final String name;
		private final int age;
		private String email = "";
		private String phone = "";
		private String address = "";

		public Builder(String name, int age) {
			this.name = name;
			this.age = age;
		}

		public Builder email(String email) { this.email = email; return this; }
		public Builder phone(String phone) { this.phone = phone; return this; }
		public Builder address(String address) { this.address = address; return this; }

		public User build() {
			if (age < 0 || age > 150) {
				throw new IllegalArgumentException("年龄不合法: " + age);
			}
			return new User(this);
		}
	}
}

// 使用:链式调用,参数自文档化
User user = new User.Builder("张三", 25)
		.email("zhangsan@example.com")
		.phone("13800138000")
		.address("北京市")
		.build();

与传统四角色模式的区别:Builder 既是建造者也是指导者,build() 是最终的校验关卡。

Lombok @Builder:注解驱动

@Builder 是 Lombok 提供的注解,编译时自动生成链式 Builder------说白了就是"简化实现"的自动挡。

java 复制代码
import lombok.Builder;
import lombok.ToString;

@Builder
@ToString
public class User {
	private String name;
	private int age;
	private String email;
	private String phone;
}

// 使用 ------ 注意是 User.builder() 而非 new User.Builder()
User user = User.builder()
		.name("张三")
		.age(25)
		.email("zhangsan@example.com")
		.build();

Lombok 编译时生成的代码反编译后大致如下:

java 复制代码
public class User {
	private String name;
	private int age;
	private String email;
	private String phone;

	// Lombok 自动生成的静态方法:对外入口
	public static UserBuilder builder() {
		return new UserBuilder();
	}

	// Lombok 自动生成的静态内部类
	public static class UserBuilder {
		private String name;
		private int age;
		private String email;
		private String phone;

		UserBuilder() {}

		public UserBuilder name(String name) { this.name = name; return this; }
		public UserBuilder age(int age) { this.age = age; return this; }
		public UserBuilder email(String email) { this.email = email; return this; }
		public UserBuilder phone(String phone) { this.phone = phone; return this; }

		public User build() {
			return new User(name, age, email, phone);
		}
	}
}

关键区别:Lombok 多生成了一个 static builder() 方法,调用方不用 new 内部类,直接 User.builder() 就行。需要自定义校验时,在 UserBuilder 里手写 build() 方法覆盖自动生成的版本,Lombok 检测到同名方法会跳过生成。

如何选择

维度 传统四角色 简化链式(手写) Lombok @Builder
构建步骤 有明确步骤和顺序 只是字段赋值,无顺序概念 同简化
Director 有,封装构建算法 无,Builder 自导
参数校验 getResult() 中校验 build() 中集中校验 需手写 build() 覆盖
代码量 多(接口+实现+Director) 中等(一个内部类) 一个注解
灵活性 高(可替换 Builder) 中(Builder 实现固定) 低(生成代码结构不可改)
本质 设计模式 设计模式的简化变体 代码生成工具

记忆口诀

简单赋值用 @Builder,复杂构建手写 Builder,算法复用上四角色。

什么时候必须手写 Builder?当构建不只是赋值,还有收集、转换、拼接逻辑时,@Builder 就搞不定了:

java 复制代码
// SQL 查询构建器:有状态 + 有拼接逻辑,@Builder 做不到
public class SqlQueryBuilder {
	private String table;
	private List<String> conditions = new ArrayList<>();
	private List<String> columns = new ArrayList<>();

	public SqlQueryBuilder from(String table) { this.table = table; return this; }

	public SqlQueryBuilder select(String... cols) {
		this.columns.addAll(Arrays.asList(cols));
		return this;
	}

	public SqlQueryBuilder where(String condition) {
		this.conditions.add(condition);
		return this;
	}

	public String build() {
		StringBuilder sql = new StringBuilder("SELECT ");
		sql.append(String.join(", ", columns));
		sql.append(" FROM ").append(table);
		if (!conditions.isEmpty()) {
			sql.append(" WHERE ").append(String.join(" AND ", conditions));
		}
		return sql.toString();
	}
}

// 使用
String sql = new SqlQueryBuilder()
		.select("name", "age")
		.from("users")
		.where("age > 18")
		.where("status = 'active'")
		.build();
// SELECT name, age FROM users WHERE age > 18 AND status = 'active'

这是 @Builder 搞不定的场景------where() 要把条件收集到列表再拼接,不是简单的字段赋值。

总结

建造者模式解决的是"复杂对象怎么一步步构建"的问题。构建步骤交给 Builder,构建顺序交给 Director,调用方只关心"我要什么"。

什么时候用

  • 对象参数多(≥4个),多数可选------构造函数重载太多,链式设置更清晰
  • 构建步骤有顺序依赖------必须先 A 后 B,方法调用顺序就是构建顺序
  • 需要创建不可变对象------构造后不能改,Builder 是收集参数的唯一入口
  • 同一流程产出不同产品------Director + 不同 Builder,构建算法复用
  • 构建时需要参数校验------在 build() 中集中校验,不用散落各处

什么时候不用

  • 简单对象(参数 < 4 个)------直接构造函数就够了
  • 构建过程无顺序依赖------Builder 的核心优势就没了

简单记忆

工厂管"产出什么",建造者管"怎么一步步造出来"。

建造者模式 vs 抽象工厂模式

  • 抽象工厂:选套餐------一个工厂出一整套产品,重点在"这组产品风格统一"
  • 建造者:排工序------同样的步骤造不同的东西,重点在"构建过程复用"
  • 一句话区分:抽象工厂返回多个产品 (产品族),建造者返回一个产品(多步构建);抽象工厂不管"怎么造",建造者不管"造几件"
维度 抽象工厂 建造者
关注点 产品之间的兼容性(同族配套) 构建过程的复用性(同算法不同产出)
产出 多个产品(一族) 一个复杂产品(多步构建)
切换维度 换工厂 = 换产品族 换 Builder = 换产品表示
新增扩展 加产品族:加一个工厂;加产品种:改所有工厂 加产品变体:加一个 Builder;构建算法不变

练习题目

多格式文档生成器

题目描述:一个文档系统需要将相同内容导出为不同格式。每份文档都由标题(Title)、段落(Paragraph)、列表(List)三部分组成,但不同格式的渲染方式完全不同:

步骤 Plain Text HTML Markdown
buildTitle === 标题 === <h1>标题</h1> # 标题
buildParagraph 直接输出文本 <p>文本</p> 直接输出文本
buildList * item1 * item2(内联) <ul><li>item1</li><li>item2</li></ul> - item1 换行 - item2

请使用建造者模式实现,Director 统一控制"标题→段落→列表"的构建顺序。

输入描述 :第一行整数 N(1 ≤ N ≤ 100),表示文档数量。每个文档占 4 行:第 1 行格式类型(plain/html/markdown),第 2 行标题文本,第 3 行段落文本,第 4 行空格分隔的列表项。

输出描述:每份文档输出对应格式的渲染结果,文档间用空行分隔。

输入示例

css 复制代码
3
plain
Design Patterns
Builder separates construction from representation
Simple Flexible Reusable
html
Design Patterns
Builder separates construction from representation
Simple Flexible Reusable
markdown
Design Patterns
Builder separates construction from representation
Simple Flexible Reusable

输出示例

css 复制代码
=== Design Patterns ===
Builder separates construction from representation
* Simple * Flexible * Reusable

<h1>Design Patterns</h1>
<p>Builder separates construction from representation</p>
<ul><li>Simple</li><li>Flexible</li><li>Reusable</li></ul>

# Design Patterns
Builder separates construction from representation
- Simple
- Flexible
- Reusable

解题思路

  1. 识别角色:Document 是产品,Builder 接口定义三个构建步骤 + getResult,三种格式各一个 ConcreteBuilder,Director 编排"标题→段落→列表"的顺序
  2. Director 的价值:不管哪种格式,构建顺序都一样。把顺序封装到 Director 里,客户端不用自己保证调用顺序,格式越多越不容易出错
  3. 产品设计 :Document 用 StringBuilder 累积内容,每次 buildXxx() 往里追加渲染后的文本,getContent() 返回最终结果
  4. 格式差异封装 :Plain Text 加 === 装饰、HTML 加标签、Markdown 加 # / - 前缀------这些差异全藏在各自的 ConcreteBuilder 里,Director 和客户端完全不用管
java 复制代码
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = Integer.parseInt(sc.nextLine());
        for (int i = 0; i < n; i++) {
            String format = sc.nextLine();
            String title = sc.nextLine();
            String paragraph = sc.nextLine();
            String[] items = sc.nextLine().split(" ");

            Builder builder;
            switch (format) {
                case "html":     builder = new HTMLBuilder();      break;
                case "markdown": builder = new MarkdownBuilder();   break;
                default:         builder = new PlainTextBuilder();  break;
            }

            Director director = new Director(builder);
            Document doc = director.construct(title, paragraph, items);
            if (i > 0) System.out.println();
            System.out.print(doc.getContent());
        }
    }
}

// ==================== 产品 ====================
class Document {
    private StringBuilder content = new StringBuilder();

    public void append(String text) {
        content.append(text);
    }

    public String getContent() {
        return content.toString();
    }
}

// ==================== 抽象建造者 ====================
interface Builder {
    void buildTitle(String title);
    void buildParagraph(String paragraph);
    void buildList(String[] items);
    Document getResult();
}

// ==================== 具体建造者:纯文本 ====================
class PlainTextBuilder implements Builder {
    private Document doc = new Document();

    @Override
    public void buildTitle(String title) {
        doc.append("=== " + title + " ===\n");
    }

    @Override
    public void buildParagraph(String paragraph) {
        doc.append(paragraph + "\n");
    }

    @Override
    public void buildList(String[] items) {
        // 内联格式:* item1 * item2 * item3
        for (int i = 0; i < items.length; i++) {
            if (i > 0) doc.append(" ");
            doc.append("* " + items[i]);
        }
        doc.append("\n");
    }

    @Override
    public Document getResult() { return doc; }
}

// ==================== 具体建造者:HTML ====================
class HTMLBuilder implements Builder {
    private Document doc = new Document();

    @Override
    public void buildTitle(String title) {
        doc.append("<h1>" + title + "</h1>\n");
    }

    @Override
    public void buildParagraph(String paragraph) {
        doc.append("<p>" + paragraph + "</p>\n");
    }

    @Override
    public void buildList(String[] items) {
        doc.append("<ul>");
        for (String item : items) {
            doc.append("<li>" + item + "</li>");
        }
        doc.append("</ul>\n");
    }

    @Override
    public Document getResult() { return doc; }
}

// ==================== 具体建造者:Markdown ====================
class MarkdownBuilder implements Builder {
    private Document doc = new Document();

    @Override
    public void buildTitle(String title) {
        doc.append("# " + title + "\n");
    }

    @Override
    public void buildParagraph(String paragraph) {
        doc.append(paragraph + "\n");
    }

    @Override
    public void buildList(String[] items) {
        for (String item : items) {
            doc.append("- " + item + "\n");
        }
    }

    @Override
    public Document getResult() { return doc; }
}

// ==================== 指导者:封装构建流程 ====================
class Director {
    private Builder builder;

    public Director(Builder builder) {
        this.builder = builder;
    }

    public Document construct(String title, String paragraph, String[] items) {
        builder.buildTitle(title);       // 第一步:构建标题
        builder.buildParagraph(paragraph); // 第二步:构建段落
        builder.buildList(items);          // 第三步:构建列表
        return builder.getResult();
    }
}

扩展:实际项目中的建造者模式

StringBuilder(JDK 标准库)

字符串拼接是多步骤操作,直接用 + 会产生大量临时对象。StringBuilder 就是最经典的 Builder------每次 append() 往内部 char 数组追加内容,toString() 一次性输出完整字符串。

java 复制代码
String result = new StringBuilder()
		.append("Hello")
		.append(" ")
		.append("World")
		.toString();
  • 产品String
  • BuilderStringBuilder
  • 构建步骤append() 方法

OkHttp 的 Request 构建

HTTP 请求有很多可选参数(header、body、method),某些组合还有依赖关系。OkHttp 用 Builder 模式把请求构建链式化,每个方法只设一个属性,读起来一目了然。

java 复制代码
Request request = new Request.Builder()
		.url("https://api.example.com/users")
		.addHeader("Authorization", "Bearer token123")
		.addHeader("Content-Type", "application/json")
		.post(RequestBody.create(
				MediaType.parse("application/json"),
				"{"name":"张三"}"
		))
		.build();

Spring Security 配置

安全配置项多且有依赖------配了 formLogin 就得同时设 loginPage。Spring Security 的 DSL 风格配置本质上就是 Builder 模式的变体,用 Lambda 嵌套表达层级关系。

java 复制代码
http
	.authorizeHttpRequests(auth -> auth
		.requestMatchers("/api/public/**").permitAll()
		.requestMatchers("/api/admin/**").hasRole("ADMIN")
		.anyRequest().authenticated()
	)
	.formLogin(form -> form
		.loginPage("/login")
		.defaultSuccessUrl("/home")
	)
	.csrf(csrf -> csrf.disable());

Lombok @Builder 在 DTO 中的应用

DTO 字段多,构造函数参数顺序容易搞错,setter 又太啰嗦------@Builder 刚好。一行注解生成完整的链式 Builder,代码量从几十行缩到一行。

java 复制代码
@Data
@Builder
public class OrderDTO {
	private String orderId;
	private String userId;
	private BigDecimal totalAmount;
	private List<OrderItem> items;
	private LocalDateTime createTime;
	private OrderStatus status;
}

// 使用
OrderDTO order = OrderDTO.builder()
		.orderId("ORD001")
		.userId("USER001")
		.totalAmount(new BigDecimal("299.99"))
		.createTime(LocalDateTime.now())
		.status(OrderStatus.PENDING)
		.build();

Guava 的不可变集合

不可变集合创建后不能修改,必须一次性构建完成。Guava 用 Builder 收集元素,build() 一次性生成不可变集合------构建过程中随便加,构建完就锁死。

java 复制代码
ImmutableList<String> list = ImmutableList.<String>builder()
		.add("Apple")
		.add("Banana")
		.add("Cherry")
		.build();

ImmutableMap<String, Integer> map = ImmutableMap.<String, Integer>builder()
		.put("one", 1)
		.put("two", 2)
		.put("three", 3)
		.build();

现在可能还用不到四角色模式,但等碰到"参数爆炸、构建步骤有顺序、需要不可变对象"的场景时,你会发现------这不就是建造者模式吗?

技术交流 & 更多原创内容,关注公众号:咖啡八杯

相关推荐
l软件定制开发工作室1 小时前
Spring开发系列教程(41)——集成Open API
java·后端·spring
传说之后1 小时前
GO语言 理解 Goroutine:使用与原理
后端
用户762352425911 小时前
Redis7 底层数据结构解析
后端
折哥的程序人生 · 物流技术专研1 小时前
《Java 100 天进阶之路》第14篇:Java final关键字详解
java·开发语言·后端·面试
IT当时语_青山师__JAVA技术栈1 小时前
数组与链表深度解析:从内存布局到工业级实践
java·算法·面试
java1234_小锋1 小时前
Spring AI 2.0 开发Java Agent智能体 - 工具调用(Function Calling / Tools)
java·人工智能·spring
Cosmoshhhyyy1 小时前
《Effective Java》解读第 52 条:慎用重载
java·开发语言·windows
大大杰哥1 小时前
温故知新:Java 线程创建方式的演进与总结
java·开发语言·jvm
凯瑟琳.奥古斯特1 小时前
死锁四大必要条件解析
java·开发语言·后端·职场和发展