Featured image of post Java API 设计中的 Fluent 与 Builder

Java API 设计中的 Fluent 与 Builder

Builder 是设计模式,Fluent 是接口风格——两个常被混为一谈的概念,本文讲清差异、典型用法与工程取舍

引子:两段看似一样的代码

1
2
3
4
5
// 写法 A
User user = new User();
user.setName("张三");
user.setAge(20);
user.setEmail("zhangsan@example.com");
1
2
3
4
5
6
// 写法 B
User user = User.builder()
        .name("张三")
        .age(20)
        .email("zhangsan@example.com")
        .build();
1
2
3
4
5
// 写法 C
User user = new User()
        .setName("张三")
        .setAge(20)
        .setEmail("zhangsan@example.com");

B 和 C 都是"链式调用",看起来差不多——但它们解决的是完全不同的问题。B 是Builder 模式,C 是Fluent Setter 风格。两者在中文社区经常被混为一谈,导致很多文章在讨论"Builder 模式"时其实展示的是 Fluent。

本文把这两个概念彻底分清楚,讲清楚各自解决什么问题、什么时候用哪个、配合 Lombok 怎么写最舒服。


一、先把概念分清楚

Fluent(流式接口)

Fluent 是一种接口设计风格——方法返回 this(或某种上下文对象),让调用可以连成一串。它本身不是设计模式,只是一种"让 API 读起来像句子"的写法。

1
2
3
4
5
6
7
8
public class StringBuilder {
    public StringBuilder append(String s) {
        // ...
        return this;   // ← 关键
    }
}

new StringBuilder().append("Hello, ").append("world").append("!");

JDK 里 StringBuilderStream 都是 Fluent 风格。

Builder(生成器模式)

Builder 是 GoF 设计模式——为构造一个复杂对象单独建一个"建造者"对象,把"对象的构造过程"和"对象本身"分开。

1
2
3
4
User u = new User.Builder()
        .name("张三")
        .age(20)
        .build();          // ← 关键:build() 才返回真正的目标对象

Builder 通常也用 Fluent 风格写——所以才容易混淆。但它的核心特征不是"链式",而是:

  1. 构造过程和目标对象分离(中间态在 Builder 上,不污染目标对象)
  2. 有一个明确的 build() 终点
  3. 目标对象通常是不可变的(构造完所有字段都 final)

二、Fluent Setter:最朴素的链式

最简单的 Fluent 写法——在 Setter 里返回 this

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class User {
    private String name;
    private Integer age;

    public User setName(String name) {
        this.name = name;
        return this;       // ← Fluent 关键
    }

    public User setAge(Integer age) {
        this.age = age;
        return this;
    }
}

new User()
    .setName("张三")
    .setAge(20);

Lombok 一个 @Accessors(chain = true) 就能生成:

1
2
3
4
5
6
@Data
@Accessors(chain = true)
public class User {
    private String name;
    private Integer age;
}

或者去掉 set 前缀让 API 更紧凑:

1
2
3
4
5
6
7
@Accessors(fluent = true)
public class User {
    private String name;
    private Integer age;
}

new User().name("张三").age(20);

优点

  • 写法简单,对原 POJO 改动最小
  • API 紧凑,链式
  • 容易加在已有项目上,渐进式改造

缺点

  • 可变对象——构造完之后还能改,线程安全性差
  • 没有"构造完成"的明确语义——任何时候都可以接着调
  • 不能做必填校验——你 new User() 之后直接用,编译期不会报错
  • 和 JavaBean 规范冲突——set 系列方法返回 void 是 JavaBean 标准,框架(早期 BeanUtils、序列化库)可能不识别

所以——Fluent Setter 更适合内部 POJO,对外 API 通常优先选 Builder 这种语义更明确的风格


三、Builder 模式:标准写法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class User {
    private final String name;
    private final Integer age;
    private final String email;

    private User(Builder b) {
        this.name = b.name;
        this.age = b.age;
        this.email = b.email;
    }

    public static Builder builder() {
        return new Builder();
    }

    public static class Builder {
        private String name;
        private Integer age;
        private String email;

        public Builder name(String name) { this.name = name; return this; }
        public Builder age(Integer age)  { this.age = age;   return this; }
        public Builder email(String e)   { this.email = e;   return this; }

        public User build() {
            // 必填校验放这里
            Objects.requireNonNull(name, "name is required");
            return new User(this);
        }
    }

    // 只暴露 getter,没有 setter
    public String getName()   { return name; }
    public Integer getAge()   { return age; }
    public String getEmail()  { return email; }
}

调用方:

1
2
3
4
5
User u = User.builder()
        .name("张三")
        .age(20)
        .email("z@x.com")
        .build();

Lombok 一个 @Builder 就能生成上面所有的代码:

1
2
3
4
5
6
7
@Builder
@Getter
public class User {
    private final String name;
    private final Integer age;
    private final String email;
}

优点

  • 目标对象不可变——所有字段 final,天然线程安全
  • build() 是明确终点——调用方知道"这里之前在配置,之后是真正的对象"
  • 必填校验集中——所有约束放 build() 一处
  • 构造逻辑可独立演化——Builder 可以加链式方法、衍生 API,目标对象保持简洁
  • 天然适合"参数多且大部分可选"的对象

缺点

  • 类多一倍(一个目标 + 一个 Builder),不用 Lombok 写起来烦
  • 字段加减时 Builder 也要改

四、什么时候用 Fluent,什么时候用 Builder

用 Fluent Setter 的场景

  • 进程内的 DTO / VO / Form——只在内部传递,不需要不可变性
  • 已有 POJO 的最小改造——加个 @Accessors(chain=true) 就完事
  • 配置类、上下文对象——会被持续修改,本就是可变的

用 Builder 的场景

  • 不可变值对象——领域模型、配置对象、消息体
  • 构造参数多(≥4 个)且大部分可选——Builder 比"重载 N 个构造器"清爽得多
  • 对外暴露的 API——Builder 给调用方明确的"配置 → 完成"语义
  • 需要必填校验——build() 是兜底

两者结合的反面教材

1
2
3
4
5
6
7
// 反面教材:既能 builder().build(),又能直接 new + setter
@Data
@Builder
public class User {
    private String name;
    private Integer age;
}

@Data 会生成 setter,@Builder 会生成 Builder——结果就是这个对象既不可变,又有 Builder。Builder 的不可变优势完全丧失。

正确写法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 不可变 + Builder
@Builder
@Getter        // 只生成 getter,没有 setter
public class User {
    private final String name;
    private final Integer age;
}

// 或者:可变 + Fluent,不要 Builder
@Data
@Accessors(chain = true)
public class UserDTO {
    private String name;
    private Integer age;
}

两种风格通常不建议混在一个类上——除非你确实需要可变 + Builder 双重能力,否则会让语义模糊。


五、Builder 进阶用法

必填字段强制:阶梯式 Builder(Step Builder)

普通 Builder 只能在 build() 里运行时校验,编译期帮不了你。Step Builder 通过类型系统让"必填字段没填就编译不过":

1
2
3
4
5
6
User u = User.builder()
        .name("张三")     // 必填
        .age(20)          // 必填
        .optional()       // 进入"可选阶段"
        .email("...")
        .build();

实现要写多个接口(NameStep → AgeStep → OptionalStep),代码量大,只有"对外 SDK,且想强制必填"时才值得用

Builder 里加业务校验

1
2
3
4
5
6
7
8
9
public User build() {
    if (age != null && age < 0) {
        throw new IllegalArgumentException("age must be >= 0");
    }
    if (email != null && !email.contains("@")) {
        throw new IllegalArgumentException("invalid email");
    }
    return new User(this);
}

所有"对象不变式"应当在 build() 里检查,让目标对象一旦构造出来就一定是合法的。

Builder 复用:从已有对象出发再修改

可变对象修改方便,不可变对象怎么办?答案是 toBuilder

1
2
3
4
5
6
7
8
9
@Builder(toBuilder = true)
@Getter
public class User {
    private final String name;
    private final Integer age;
}

User u1 = User.builder().name("张三").age(20).build();
User u2 = u1.toBuilder().age(21).build();   // 创造一个"修改了 age 的副本"

toBuilder() 把现有对象的所有字段拷进 Builder,让你像"对象修改"一样,但底层每次都是创建新对象——既保留了不可变,又方便"小改造"。

集合字段:@Singular

参数里有集合的话,Lombok 提供 @Singular

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Builder
public class Order {
    private final String orderNo;
    @Singular
    private final List<OrderItem> items;
}

Order o = Order.builder()
        .orderNo("123")
        .item(item1)        // 单个元素
        .item(item2)
        .items(List.of(...))// 批量
        .build();

六、Fluent 的另外两个高级形态

1. Method Chaining 操作集合:Stream

1
2
3
4
5
List<String> result = users.stream()
        .filter(u -> u.getAge() > 18)
        .map(User::getName)
        .sorted()
        .collect(toList());

Stream 的每一步都返回 Stream<T>,看起来像 Fluent,但本质是返回新对象而非 this。这是 Fluent 的"不可变变体"——每个操作产生新流,原流不变。这种"Fluent + 不可变"的组合在函数式风格里非常常见

2. 内部 DSL:建造一棵描述树

很多框架用 Fluent 风格搭出一个"Domain Specific Language":

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// MyBatis-Plus QueryWrapper
new LambdaQueryWrapper<User>()
    .eq(User::getStatus, 1)
    .like(User::getName, "张")
    .orderByDesc(User::getCreateTime);

// Mockito
when(repo.findById(1L))
    .thenReturn(Optional.of(user));

// AssertJ
assertThat(users)
    .hasSize(2)
    .extracting(User::getName)
    .containsExactly("张三", "李四");

Fluent 的真正威力在这里——让代码读起来像自然语言/SQL,远超普通 setter 链能做到的。


七、踩坑提醒

  1. @Data + @Builder 反模式——前面说过,要么不可变 + Builder,要么可变 + Fluent,别叠加
  2. @SuperBuilder 用于继承——@Builder 不支持父子类,要用 @SuperBuilder
  3. Fluent Setter 和 JavaBean 规范冲突——一些老库(如 Apache BeanUtils)按"setter 返回 void"识别,看不到 Fluent setter;序列化库通常没问题(用反射)
  4. 链式调用的报错堆栈不友好——一行长链报错时只能看到行号,建议合理换行
  5. 不要做"链式校验"——把 validate() 加进链里看似优雅,实际让链式表达力变差,用 build() 集中校验
  6. 不可变对象的 equals/hashCode——使用 @Value 或者 @EqualsAndHashCode,否则 Map / Set 行为会出错

八、一个工程上的"风格指南"

我自己在团队里推的一份简版规则:

类型推荐风格
领域模型 / 值对象@Builder + @Getter,不可变
API 请求/响应 DTO@Data + @Accessors(chain=true),可变 + Fluent
配置类@Builder + @Getter,不可变
内部 DTO/VO视情况用 Fluent,不需要 Builder
框架配置/上下文视情况用 Builder,明确"build 完成"语义

把"什么对象用什么风格"约定下来,比"代码漂亮"重要得多。


小结

把全文压一句:

Fluent 是 API 风格——让代码读起来像句子;Builder 是设计模式——让复杂对象的构造和不可变共存。前者解决可读性,后者解决可维护性。

记住三个判断:

  1. 要不要不可变? 要 → Builder;不要 → Fluent Setter
  2. 构造参数多不多? 多(≥4)→ Builder;少 → 普通构造器
  3. 是不是对外 API? 是 → Builder(明确语义);否 → 都行

把概念拎清楚,下次再看到 obj.a().b().c().build() 这种代码,你就能一眼看出它是真 Builder 还是 Fluent 冒充的,也就能写出更精准的 API。

使用 Hugo 构建
主题 StackJimmy 设计