配套代码:GitHub 仓库 · 本篇完整源码 · 行为测试。使用 JDK 25 与 Gradle,包名为 com.hanserwei.patterns.builder

系列导航:Java 25 设计模式学习指南

上一篇:抽象工厂 · 下一篇:原型

文章需要必填标题,也可以附带多个标签。继续扩充构造器会出现多个相似签名,调用处难以判断每个参数含义;如果先 new 空文章再调用 setter,又会让“标题尚未设置”的对象流入业务层。

我们希望构建过程允许分步配置,但交给业务层的 Article 从诞生起就是有效、独立的对象。构建器承担临时可变状态,文章承担稳定的结果状态。

从问题提炼设计意图

建造者分离复杂对象的逐步构建与最终表示。本例使用 Java 常见的嵌套 Builder 变体,通过 build 校验并产生不可变结果。

对象职责与协作关系

示例角色 职责
Article 最终产品,只暴露读取能力
Article.Builder 构建器,收集必填项与可选标签
Demo 装配方,组织具体构建步骤
classDiagram
    Article ..> Builder : reads validated state
    Builder ..> Article : builds
    Demo ..> Builder : configures

代码思路:变化应该落在哪个对象上

标题作为 Builder 构造器参数出现,使必填项在调用处显式可见。addTag 返回当前构建器,允许链式调用;它不创建 Article,而是继续修改构建阶段的参数。

build 是有效对象的边界。空白标题在这里失败,成功时调用 Article 的私有构造器。产品没有公开 setter,字段是 private final,因此“修改配置”与“使用文章”两个阶段拥有不同的 API。

真正保证标签独立的是 List.copyOf,而不只是 final。final 只禁止字段重新指向其他列表;如果把 builder.tags 直接赋给 article.tags,继续调用 addTag 仍能修改旧文章。复制之后,重用同一个构建器创建多个结果也不会相互污染。

关键实现与独立运行

配套仓库中的包名是 com.hanserwei.patterns.builder,源码目录为 src/main/java/com/hanserwei/patterns/builder/。仓库地址统一见系列导航。以下展示关键文件的完整内容;其余角色和测试在同一仓库中,每个顶级类型各占一个文件。

Article.java

package com.hanserwei.patterns.builder;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/** 由构建器校验并创建的不可变文章. */
public final class Article {
  /** 发布后不再变化的标题. */
  private final String title;

  /** 构造时复制的不可变标签集合. */
  private final List<String> tags;

  /** 从已校验的构建器取得快照,避免后续构建影响旧对象. */
  private Article(Builder builder) {
    title = builder.title;
    tags = List.copyOf(builder.tags);
  }

  /** 返回文章标题. */
  public String title() {
    return title;
  }

  /** 返回不可修改的标签快照. */
  public List<String> tags() {
    return tags;
  }

  /** 收集可选参数并在最终创建时维护文章不变量. */
  public static final class Builder {
    /** 必填标题,构造完成后不允许替换. */
    private final String title;

    /** 构建过程中的可变标签,仅由构建器持有. */
    private final List<String> tags = new ArrayList<>();

    /** 指定必填标题;空白标题在 build 时统一拒绝. */
    public Builder(String title) {
      this.title = Objects.requireNonNull(title, "title");
    }

    /** 添加非空白标签,并返回构建器以继续配置. */
    public Builder addTag(String tag) {
      if (tag == null || tag.isBlank()) {
        throw new IllegalArgumentException("Tag must not be blank");
      }
      tags.add(tag);
      return this;
    }

    /** 校验标题并创建独立文章快照. */
    public Article build() {
      if (title.isBlank()) {
        throw new IllegalArgumentException("Title must not be blank");
      }
      return new Article(this);
    }
  }
}

Demo.java 展示调用方如何装配这些对象:

package com.hanserwei.patterns.builder;

/** 演示本章对象的装配方式和可观察结果. */
public final class Demo {
  /** 禁止实例化演示入口. */
  private Demo() {}

  /** 运行独立示例;args 为未使用的命令行参数. */
  public static void main(String[] args) {
    Article article = new Article.Builder("Java 25").addTag("OOP").build();
    System.out.println(article.title() + article.tags());
  }
}

在配套代码仓库根目录运行;Windows 使用 gradlew.bat 替换 ./gradlew

./gradlew runBuilder
./gradlew test --tests 'com.hanserwei.patterns.builder.PatternTest'

示例的业务输出如下,省略 Gradle 自身的任务提示:

Java 25[OOP]

用测试确认模式的行为

重用构建器不污染旧对象;返回列表不能修改;空白标题无法 build。三个断言分别守住快照隔离、只读边界和业务有效性。

对应测试位于 src/test/java/com/hanserwei/patterns/builder/PatternTest.java。建议先运行现有测试,再改动一个协作环节,观察哪个断言能够发现问题。

常见用法

  • 请求配置、查询条件和文章等对象包含多个可选参数。
  • 构造过程要分阶段收集数据,最终统一校验跨字段规则。
  • 需要创建不可变对象,同时让调用方拥有易读的配置接口。

适用边界与容易踩的坑

GoF 的经典建造者还会把构建步骤抽象成 Builder 接口,并让 Director 复用步骤序列,生成不同产品表示。本例没有 Director,也没有多种产品表示;它是更轻量的 Java Builder 用法,不应把链式调用本身当成完整定义。

如果对象只有两个必填字段、没有复杂校验,一个普通构造器通常更清楚。Builder 增加了类、状态和失败时机;“每个类都带 Builder”不是 OOP 的要求。

List.copyOf 只复制容器,不会递归复制可变元素。本例标签是不可变 String,因此足够;如果标签变成带可变属性的对象,还必须规定它们的复制或不可变策略。构建器本身也不承诺线程安全。

与相近模式比较

简单工厂主要选择产品类型;建造者主要组织一个产品的构建步骤。原型从现有对象的状态出发复制,建造者则从参数和步骤出发创建。

动手练习

增加可选摘要和“摘要长度不得超过标题长度三倍”的规则,在 build 中校验跨字段关系。再实现一个 Director,为每周刊物复用固定标签配置,并解释它应该持有构建器实例还是每次创建新的构建器。

系列导航:Java 25 设计模式学习指南

上一篇:抽象工厂 · 下一篇:原型