Java 设计模式学习网站:Java设计模式:23种设计模式全面解析(超级详细)
知乎专栏:秒懂设计模式之建造者模式(Builder pattern)
《Effective Java 中文版(原书第 3 版)》 —— 俞黎敏译
1. 模式的定义与特点
参考链接:建造者模式(Bulider模式)详解
建造者(Builder)模式(又称构建者模式):将复杂对象的构造过程与其表示形式分离,使同一构造过程创建出不同的表示形式。它是将一个复杂的对象分解为多个简单的对象,然后一步一步构建而成。它将变与不变相分离,即产品的组成部分是不变的,但每一部分是可以灵活选择的。
优点
- 封装性好,构建和表示分离
- 扩展性好,各个具体的建造者相互独立,有利于系统的解耦
- 客户端不必知道产品内部组成的细节,建造者可以对创建过程逐步细化,而不对其它模块产生任何影响,便于控制细节风险。
缺点
- 产品的组成部分必须相同,这限制了其使用范围
- 如果产品的内部结构复杂,当产品内部发生变化时,建造者也要同步修改,后期维护成本较大
- 为了创建对象,必须先创建它的构建器,在十分注重性能的情况下,这是一笔性能开销
建造者(Builder)模式和工厂模式的关注点不同:建造者模式注重零部件的组装过程,而工厂方法模式更注重零部件的创建过程,但两者可以结合使用。
每个字都认识,但是合在一起就不理解了,这到底是个啥? 🤔
不明白没有关系,这段文字可记可不记,当然记住更好,以后和别人解释就有装逼的资格了。🤣
2. 适用场景
设计模式,最重要的是如何使用和适用场景。
在《Effective Java 中文版(原书第 3 版)》一书中的第 2 条就说到了:遇到多个构造器(Constructor)参数时要考虑使用构建器(Builder)。
说人话就是:如果一个类的构造函数参数有多个时,要考虑使用构造器(Builder)。
其实还是有点模糊,再说人话:当一个类的构造函数参数个数超过 4 个,而且某些参数是可选的参数,考虑使用建造者模式。
问题又来了:
- 建造者模式解决了哪些问题?
- 为什么要使用建造者模式?
- 怎么使用建造者模式?
3. 解决的问题
假设有这样一个 Person 类:
/**
* @author mofan 2021/2/3
*/
public class Person {
private String firstName;
private String lastName;
private Integer gender;
private Integer age;
private Double height;
private Double weight;
private String job;
}
在创建 Person 对象时,有些属性是必填的,比如 firstName、lastName 和 gender,其余属性是可选的。
那现在想要创建一个这样的 Person 对象:
firstName ---> 默 lastName ---> 烦 gender ---> 1 age ---> 19 height ---> 178.2
该怎么做呢?
3.1 重叠构造器模式
在《Effective Java 中文版(原书第 3 版)》一书中,首先提到了 重叠构造器模式,即创建一个包含需要设置的参数的构造函数。
public Person(String firstName, String lastName, Integer gender, Integer age, Double height) {
this.firstName = firstName;
this.lastName = lastName;
this.gender = gender;
this.age = age;
this.height = height;
}
但这种方式是很愚蠢的 😑
-
如果需求发生变更,想创建一个还包含
job属性的对象,则又要提供一个包含job参数的构造函数;同样,如果想创建一个缺少某个属性的对象,也要再提供一个构造函数 -
使用构造函数时,构造函数的参数有很多,但有很多相同的类型,在设置时可能会混淆。比如
gender和age都是Integer类型的,如果不小心将它们的值设置反了,这个错误不会在编译器体现,后续需要额外的时间去纠错
总之,采用 重叠构造器模式 来解决并不是一种好的方式。
3.2 JavaBeans 模式
在《Effective Java 中文版(原书第 3 版)》一书中,紧接着又提到了 JavaBeans 模式。
简单来说:先调用一个无参构造函数来创建对象,然后使用 Setter 方法来设置必须与可选参数。
public Person() {}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public void setGender(Integer gender) {
this.gender = gender;
}
public void setAge(Integer age) {
this.age = age;
}
public void setHeight(Double height) {
this.height = height;
}
public void setWeight(Double weight) {
this.weight = weight;
}
public void setJob(String job) {
this.job = job;
}
这种方式也存在问题 😔
因为对象的各个属性值是分步设置的,这个过程中对象的状态容易发生变化,进而产生错误。
《Effective Java 中文版(原书第 3 版)》中是这么说的:
遗憾的是, JavaBeans 模式自身有着很严重的缺点。 因为构造过程被分到了几个调用中,在构造过程中 JavaBean 可能处于不一致的状态。 类无法仅仅通过检验构造器参数的有效性来保证一致性。 试图使用处于不一致状态的对象将会导致失败,这种失败与包含错误的代码大相径庭,因此调试起来十分困难。 与此相关的另一点不足在于, JavaBeans 模式使得把类做成不可变的可能性不复存在,这就需要程序员付出额外的努力来确保它的线程安全。
那有没有究极方案呢?
可以使用建造者模式! 💪
4. 建造者模式
4.1 如何使用
以 Person 为例:
- 在
Person中创建一个静态嵌套类Builder,并将Person中的参数都复制到Builder类中 - 在
Person中提供一个非public的构造函数,参数类型为Builder - 在
Builder中提供一个public的构造函数,参数为构造Person对象时的必填参数 - 在
Builder中提供多个设置方法,用于对 Person 中可选参数进行赋值,返回值为Builder类型的实例(用于链式调用) - 在
Builder中提供一个build()方法(当然叫啥名都可以,作为构建方法),用于构建最终的Person实例并返回
4.2 代码展示
/**
* @author mofan 2021/2/3
*/
public class Person {
private String firstName;
private String lastName;
private Integer gender;
private Integer age;
private Double height;
private Double weight;
private String job;
private Person(Builder builder) {
this.firstName = builder.firstName;
this.lastName = builder.lastName;
this.gender = builder.gender;
this.age = builder.age;
this.height = builder.height;
this.weight = builder.weight;
this.job = builder.job;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public Integer getGender() {
return gender;
}
public Integer getAge() {
return age;
}
public Double getHeight() {
return height;
}
public static class Builder {
private String firstName;
private String lastName;
private Integer gender;
private Integer age;
private Double height;
private Double weight;
private String job;
public Builder(String firstName, String lastName, Integer gender) {
this.firstName = firstName;
this.lastName = lastName;
this.gender = gender;
}
public Builder setAge(Integer age) {
this.age = age;
return this;
}
public Builder setHeight(Double height) {
this.height = height;
return this;
}
public Builder setWeight(Double weight) {
this.weight = weight;
return this;
}
public Builder setJob(String job) {
this.job = job;
return this;
}
public Person build() {
return new Person(this);
}
}
}
@Test
public void testBuilder() {
Person person = new Person.Builder("默", "烦", 1)
.setAge(19)
.setHeight(178.2)
.build();
assertNotNull(person);
assertEquals("默", person.getFirstName());
assertEquals("烦", person.getLastName());
assertEquals(1, person.getGender().intValue());
assertEquals(19, person.getAge().intValue());
assertEquals(178.2, person.getHeight(), 0.0);
}
上述这种方式是在 Java 中简化的使用方式,经典的建造者模式与其有所不同,不感兴趣可以跳过第 5 节。😜
5. 经典建造者模式
5.1 角色与结构
建造者(Builder)模式由产品、抽象建造者、具体建造者、指挥者等 4 个角色构成。
建造者(Builder)模式的主要角色如下:
- 产品角色(Product):它是包含多个组成部件的复杂对象,由具体建造者来创建其各个零部件。
- 抽象建造者(Builder):它是一个包含创建产品各个子部件的抽象方法的接口,通常还包含一个返回复杂产品的方法
getResult()。 - 具体建造者(Concrete Builder):实现 Builder 接口,完成复杂产品的各个部件的具体创建方法。
- 指挥者(Director):它调用建造者对象中的部件构造与装配方法完成复杂对象的创建,在指挥者中不涉及具体产品的信息。
建造者模式的结构图:

5.2 代码展示
目标
Person2类
/**
* @author mofan 2021/2/3
*/
public class Person2 {
private String firstName;
private String lastName;
private Integer gender;
private Integer age;
private Double height;
private Double weight;
private String job;
/**
* 含有必填属性的构造函数
*/
public Person2(String firstName, String lastName,
Integer gender) {
this.firstName = firstName;
this.lastName = lastName;
this.gender = gender;
}
/**
* 可选属性的 Setter 方法
*/
public void setAge(Integer age) {
this.age = age;
}
/**
* 可选属性的 Setter 方法
*/
public void setHeight(Double height) {
this.height = height;
}
/**
* 可选属性的 Setter 方法
*/
public void setWeight(Double weight) {
this.weight = weight;
}
/**
* 可选属性的 Setter 方法
*/
public void setJob(String job) {
this.job = job;
}
// 省略所有属性的 Getter 方法
}
抽象建造者类
/**
* @author mofan 2021/2/3
*/
public abstract class Person2Builder {
public abstract void setAge();
public abstract void setHeight();
public abstract void setWeight();
public abstract void setJob();
public abstract Person2 getPerson2();
}
实体建造者类,可以根据需求构建出多个实体建造者类,在此只构建一个
Student类:
/**
* @author mofan 2021/2/3
*/
public class Student extends Person2Builder {
private Person2 person2;
public Student(String firstName, String lastName,
Integer gender) {
person2 = new Person2(firstName, lastName, gender);
}
@Override
public void setAge() {
person2.setAge(19);
}
@Override
public void setHeight() {
person2.setHeight(178.2);
}
@Override
public void setWeight() {
person2.setWeight(125.6);
}
@Override
public void setJob() {
person2.setJob(null);
}
@Override
public Person2 getPerson2() {
return person2;
}
}
指导者类
/**
* @author mofan 2021/2/3
*/
public class Person2Director {
public void makePerson2(Person2Builder builder) {
/*
* 要为目标实体设置那些可选参数就在此设置
* 比如,我只想让我的目标实体具有 age 和 height 两个可选属性值
*/
builder.setAge();
builder.setHeight();
}
}
使用与测试
@Test
public void testPerson2() {
Person2Director director = new Person2Director();
Student student = new Student("默", "烦", 1);
director.makePerson2(student);
Person2 person2 = student.getPerson2();
assertEquals("默", person2.getFirstName());
assertEquals("烦", person2.getLastName());
assertEquals(1, person2.getGender().intValue());
assertEquals(19, person2.getAge().intValue());
assertEquals(178.2, person2.getHeight(), 0.0);
}
思考
相比于经典建造者模式,最初的建造者模式省略了 Director 这个角色,将构建算法交给了 Client 端,其次将 Builder 写到了要构建的产品类里面,最后采用了链式调用。
上述实现中的 Student 对象的属性值都是无法修改的,因此建造者模式也常用于创建 一个成员变量不可变的对象。所以在最初的建造者模式中,常把 Person 的属性设置成 final 的。如:
public class Person {
private final String firstName;
private final String lastName;
private final Integer gender;
private final Integer age;
private final Double height;
private final Double weight;
private final String job;
// --snip--
}
6. Lombok 中的 @Builder
6.1 基本使用
建造者模式的实现步骤都差不多,就像一个模板一样。
在 Lombok 中提供了 @Getter 和 @Setter 注解来解决冗杂 Getter 和 Setter 方法,它还提供了 @Builder 注解来一键生成建造者模式的代码。
创建一个新实体 Animal,并使用 @Builder 注解:
/**
* @author mofan 2021/2/3
*/
@Builder
@Getter
public class Animal {
private final String name;
private final String type;
private final Integer age;
private final Integer gender;
}
/**
* @author mofan 2021/2/3
*/
public class AnimalTest {
@Test
public void testAnimal() {
Animal animal = Animal.builder()
.name("小黑")
.gender(1)
.type("Dog")
.build();
assertEquals("小黑", animal.getName());
assertEquals(1, animal.getGender().intValue());
assertEquals("Dog", animal.getType());
assertNull(animal.getAge());
}
}
查看 IDEA 反编译 Animal.class 后的内容:
public class Animal {
private final String name;
private final String type;
private final Integer age;
private final Integer gender;
Animal(String name, String type, Integer age, Integer gender) {
this.name = name;
this.type = type;
this.age = age;
this.gender = gender;
}
public static Animal.AnimalBuilder builder() {
return new Animal.AnimalBuilder();
}
public String getName() {
return this.name;
}
public String getType() {
return this.type;
}
public Integer getAge() {
return this.age;
}
public Integer getGender() {
return this.gender;
}
public static class AnimalBuilder {
private String name;
private String type;
private Integer age;
private Integer gender;
AnimalBuilder() {
}
public Animal.AnimalBuilder name(String name) {
this.name = name;
return this;
}
public Animal.AnimalBuilder type(String type) {
this.type = type;
return this;
}
public Animal.AnimalBuilder age(Integer age) {
this.age = age;
return this;
}
public Animal.AnimalBuilder gender(Integer gender) {
this.gender = gender;
return this;
}
public Animal build() {
return new Animal(this.name, this.type, this.age, this.gender);
}
public String toString() {
return "Animal.AnimalBuilder(name=" + this.name + ", type=" + this.type + ", age=" + this.age + ", gender=" + this.gender + ")";
}
}
}
与前文的建造者模式的实现相比,Lombok 生成的代码添加了静态方法 builder(),并且 AnimalBuilder 的构造函数不再是 public,仅仅是包私有的。
这样在构建 Animal 对象时不再需要使用关键词 new,而是直接使用静态方法 builder()。
6.2 进一步改进
如果只用 @Builder 注解,可以看到 Lombok 为 Animal 类生成的构造函数是包私有的。
之所以用建造者模式,是希望用户 只用 建造者模式提供的方法去创建实例,包私有的构造函数,可以被同一包中的类调用,所以需要将这个构造函数设置为 private:
@AllArgsConstructor(access = AccessLevel.PRIVATE)
最终的 Animal 类:
@Builder
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Animal {
private final String name;
private final String type;
private final Integer age;
private final Integer gender;
}
重新编译后,生成的构造函数:
private Animal(String name, String type, Integer age, Integer gender) {
this.name = name;
this.type = type;
this.age = age;
this.gender = gender;
}
6.3 可以进行的拓展
使用 Lombok 的 @Builder 后虽然生成了建造者模式的结构,也可以实现属性值的选择性设置,但并没做到属性值的必须设置。要想实现也很简单只需要:
/**
* @author mofan 2021/2/3
*/
@Builder(builderMethodName = "hiddenBuilder")
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Animal {
private final String name;
private final String type;
private final Integer age;
private final Integer gender;
public static AnimalBuilder builder(String type) {
return hiddenBuilder().type(type);
}
}
使用 @Builder 注解的 builderMethodName 属性为自动生成的 builder 方法设置一个新的名字。
然后自己编写一个方法,在这个方法中调用生成的方法,并设置必填值。
@Test
public void testAnimal() {
Animal animal = Animal.builder("Dog")
.name("小黑")
.gender(1)
.build();
assertEquals("小黑", animal.getName());
assertEquals(1, animal.getGender().intValue());
assertEquals("Dog", animal.getType());
assertNull(animal.getAge());
}
重新编译后,生产的部分内容为:
// 我们自己编写的代码
public static Animal.AnimalBuilder builder(String type) {
return hiddenBuilder().type(type);
}
// 自动生成的代码
public static Animal.AnimalBuilder hiddenBuilder() {
return new Animal.AnimalBuilder();
}
6.4 代码兼容
无参构造与全参构造
如果没有显式声明一个类的构造函数,默认会有一个无参构造函数,但如果声明了一个有参构造函数,这个默认的无参构造函数就不存在了,除非显式声明它。
假设一个类在最初设计时并没有使用 @Builder 注解,在后续编码中突然使用了这个注解,由于 @Builder 注解会产生全参构造函数,为了能够更好地兼容旧代码,需要再使用 @NoArgsConstructor 注解为其声明无参构造函数。
这样显式声明无参构造函数后,@Builder 默认产生的全参构造函数就会失效,导致编译报错,因此还需要添加 @AllArgsConstructor(access = AccessLevel.PRIVATE) 注解。
默认值的处理
如果类中的属性存在默认值,这时再使用了 @Builder 注解,设置的默认值会失效。
这是因为 Lombok 生成的静态嵌套类中的属性不包含默认值。
为了让这些默认值生效,且能够愉快地使用 @Builder 注解,只需要在设置了默认值的字段上使用 @Builder.Default 注解即可。
7. 在类层次结构中使用
建造者模式也适用于类层次结构。抽象类有抽象的 builder,具体类有具体的 builder。
以下代码来自《Effective Java 中文版(原书第 3 版)》 一书。
声明枚举 Topping,表示披萨的各种馅料:
/**
* @author mofan
* @date 2022/10/5 12:34
*/
public enum Topping {
/**
* 火腿
*/
HAM,
/**
* 蘑菇
*/
MUSHROOM,
/**
* 洋葱
*/
ONION,
/**
* 胡椒
*/
PEPPER,
/**
* 香肠
*/
SAUSAGE
}
用一个抽象类表示各种各样的披萨:
/**
* @author mofan
* @date 2022/10/5 12:34
*/
@Getter
public abstract class Pizza {
final Set<Topping> toppings;
abstract static class Builder<T extends Builder<T>> {
EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
public T addTopping(Topping topping) {
toppings.add(Objects.requireNonNull(topping));
return self();
}
public abstract Pizza build();
protected abstract T self();
}
public Pizza(Builder<?> builder) {
this.toppings = builder.toppings.clone();
}
}
现有两种具体的披萨,分别是经典纽约风味的披萨和馅料内置的半月型披萨。声明枚举 Size,表示披萨的尺寸:
/**
* @author mofan
* @date 2022/10/5 13:21
*/
public enum Size {
/**
* 小
*/
SMALL,
/**
* 中
*/
MEDIUM,
/**
* 大
*/
LARGE
}
经典纽约风味的披萨的实现:
/**
* @author mofan
* @date 2022/10/5 13:22
* <p>
* 经典纽约风味披萨
*/
@Getter
public class NyPizza extends Pizza {
private final Size size;
public static class Builder extends Pizza.Builder<Builder> {
private final Size size;
public Builder(Size size) {
this.size = Objects.requireNonNull(size);
}
@Override
public NyPizza build() {
return new NyPizza(this);
}
@Override
protected Builder self() {
return this;
}
}
public NyPizza(Builder builder) {
super(builder);
this.size = builder.size;
}
}
馅料内置的半月型披萨的实现:
/**
* @author mofan
* @date 2022/10/5 14:06
*
* 馅料内置的半月型披萨
*/
@Getter
public class Calzone extends Pizza {
/**
* 是否有酱汁
*/
private final boolean sauceInside;
public static class Builder extends Pizza.Builder<Builder> {
private boolean sauceInside = false;
public Builder sauceInside() {
this.sauceInside = true;
return this;
}
@Override
public Calzone build() {
return new Calzone(this);
}
@Override
protected Builder self() {
return this;
}
}
public Calzone(Builder builder) {
super(builder);
this.sauceInside = builder.sauceInside;
}
}
@Test
public void testPizza() {
NyPizza pizza = new NyPizza.Builder(Size.SMALL)
.addTopping(Topping.SAUSAGE)
.addTopping(Topping.ONION)
.build();
assertEquals(Size.SMALL, pizza.getSize());
assertTrue(pizza.getToppings().containsAll(new HashSet<>(Arrays.asList(Topping.SAUSAGE, Topping.ONION))));
Calzone calzone = new Calzone.Builder()
.addTopping(Topping.HAM)
.sauceInside()
.build();
assertTrue(calzone.isSauceInside());
assertEquals(1, calzone.getToppings().size());
assertTrue(calzone.getToppings().contains(Topping.HAM));
}
8. 总结
建造者模式适用的场景
-
当一个类的构造函数参数个数较多(超过 4 个),而且某些参数是可选的,或者某些参数又是必选的。
-
要创造的对象是一个成员变量不可变的对象。
理解 创建的对象是一个成员变量不可变的对象
在经典建造者模式中,在具体建造者(Concrete Builder)中将属性值写死,导致属性值不可变,这里可以体现成员变量不可变。
在最初的建造者模式中,没有采用 JavaBeans 模式的方式给对象的属性赋值,因此类中没有提供任何字段的 Setter 方法,创建出来的对象的字段值无法再被修改。
因此,在使用建造者模式时,常用 final 修饰字段。
如何实现
- 参考第 4 节
- 使用 Lombok 的
@Builder和@AllArgsConstructor注解
如果一个类中有大量成员变量,但它们并不是不可变的对象,还可以建造者模式吗?
这没有标准答案,因为既可以把对字段的赋值过程改成链式的,也可以使用 Lombok 中的 @Accessors(chain = true) 实现链式调用,当然使用建造者模式也没问题。
说句题外话,有些第三方类库或框架不支持链式调用,因此 Lombok 的 @Accessors(chain = true) 注解不要随意使用。
简而言之:如果创建一个对象所用的构造器(Constructor)或静态工厂有多个参数时,可以考虑使用建造者模式;如果这些参数有部分必选的,还有部分是可选的,那就更应该考虑建造者模式。
9. 补充:链式调用新思路
为实现链式调用,常常需要使 Setter 返回 this,或者使用 Lombok 的 @Accessors(chain = true) 注解。两种方式实现的链式调用在编译后生成的 class 文件中的 Setter 都返回了 this,但这在某些情况会出现问题,比如使用 CGLib 的 BeanCopier 进行 Bean 拷贝时会因为 Setter 返回了 this 导致拷贝失败。
在这种情况下除了使用建造者模式外,还有其他的方法吗?
/**
* 链式调用的新思路
* 我并不认为这是建造者模式的通用写法
*
* @author mofan
* @date 2022/11/24 22:08
*/
public class Builder<T> {
private final Supplier<T> CONSTRUCTOR;
private Consumer<T> injector = (t) -> { };
private Builder(Supplier<T> constructor) {
this.CONSTRUCTOR = constructor;
}
public static <T> Builder<T> builder(Supplier<T> constructor) {
return new Builder<>(constructor);
}
public <F> Builder<T> with(BiConsumer<T, F> biConsumer, F f) {
Consumer<T> c = instance -> biConsumer.accept(instance, f);
injector = injector.andThen(c);
return this;
}
public T build() {
T t = CONSTRUCTOR.get();
injector.accept(t);
return t;
}
}
测试一下:
@Test
public void test() {
Student student = Builder.builder(Student::new)
.with(Student::setName, "mofan")
.with(Student::setAge, 20)
.build();
Assert.assertEquals("mofan", student.getName());
Assert.assertEquals(20, student.getAge());
}
有人管这个 Builder 叫做通用 Builder,试图将其与建造者模式联系上,但我认为这个 Builder 与建造者模式没有什么关系。
首先没法做到在创建对象时让一些参数必填,一些参数可选。或许可以提供多个 Builder#builder() 方法的重载?
但这样需要目标类中提供公有的有参构造函数,除此之外,在后续的操作中还要求目标类提供共有的 Setter,让创建出的对象能被随意修改。无论是公有的有参构造函数,还是公用的 Setter,这些在建造者模式中都不应该向外部暴露。
并不是说这个 Builder 一无是处,正如标题写的那样,这为链式调用提供了一种新思路。
10. 补充:Step Builder
参考链接:Step builder pattern
可以认为 Step Builder 是普通 Builder 的升级版,它用于逐步构建复杂对象,提供流式接口来创建具有大量可配置参数的对象。
Step Builder 不仅具有建造者模式的优点,对象的构建过程也更加清晰, 用户只会看到下一步可用的方法, 直到构建对象的正确时间才能看到构建方法。
Step Builder 并不是完美的,实现其功能的代码可读性较低,并且没有类似 Lombok 的第三方库来帮助代码生成。
10.1 需求场景
现有一 Email 类:
@Getter
public class Email {
private final EmailAddress from;
private final List<EmailAddress> to;
private final List<EmailAddress> cc;
private final List<EmailAddress> bcc;
private final String subject;
private final String content;
}
在构造 Email 时,希望能按照一定的顺序完成,比如第一步只能设置 from 的值,第二步只能设置 to 的值。
构造 Email 的过程如下,紫色表示必填值,黄色表示可选值:

10.2 代码实现
/**
* @author mofan
* @date 2024/1/12 17:58
* @link <a href="http://www.svlada.com/step-builder-pattern/">Step builder pattern</a>
*/
@Getter
public class Email {
private final EmailAddress from;
private final List<EmailAddress> to;
private final List<EmailAddress> cc;
private final List<EmailAddress> bcc;
private final String subject;
private final String content;
public static FromStep builder() {
return new Builder();
}
public sealed interface FromStep permits Builder {
ToStep from(EmailAddress from);
}
public sealed interface ToStep permits Builder {
SubjectStep to(List<EmailAddress> to);
}
public sealed interface SubjectStep permits Builder {
ContentStep subject(String subject);
}
public sealed interface ContentStep permits Builder {
Build content(String content);
}
public sealed interface Build permits Builder {
/**
* 构造 Email 对象
*/
Email build();
/**
* cc 是可选的
*/
Build cc(List<EmailAddress> cc);
/**
* bcc 也是可选的
*/
Build bcc(List<EmailAddress> bcc);
}
private Email(Builder builder) {
this.from = builder.from;
this.to = builder.to;
this.cc = builder.cc;
this.bcc = builder.bcc;
this.subject = builder.subject;
this.content = builder.content;
}
public final static class Builder implements FromStep, ToStep, SubjectStep, ContentStep, Build {
private EmailAddress from;
private List<EmailAddress> to;
private String subject;
private String content;
private final List<EmailAddress> cc = new ArrayList<>();
private final List<EmailAddress> bcc = new ArrayList<>();
@Override
public Email build() {
return new Email(this);
}
@Override
public ToStep from(EmailAddress from) {
Objects.requireNonNull(from);
this.from = from;
return this;
}
@Override
public SubjectStep to(List<EmailAddress> to) {
if (CollectionUtils.isEmpty(to)) {
throw new RuntimeException();
}
this.to = new ArrayList<>(to);
return this;
}
@Override
public ContentStep subject(String subject) {
Objects.requireNonNull(subject);
this.subject = subject;
return this;
}
@Override
public Build content(String content) {
Objects.requireNonNull(content);
this.content = content;
return this;
}
@Override
public Build cc(List<EmailAddress> cc) {
this.cc.addAll(CollectionUtils.emptyIfNull(cc));
return this;
}
@Override
public Build bcc(List<EmailAddress> bcc) {
this.bcc.addAll(CollectionUtils.emptyIfNull(bcc));
return this;
}
}
}
@Test
public void testBuild() {
Email email = Email.builder()
.from(EmailAddress.from("A"))
.to(List.of(EmailAddress.from("B"), EmailAddress.from("C")))
.subject("subject")
.content("content")
.bcc(List.of(EmailAddress.from("D")))
.cc(List.of(EmailAddress.from("E")))
.build();
assertThat(email.getFrom().getAddress()).isEqualTo("A");
assertThat(email.getTo())
.map(EmailAddress::getAddress)
.containsExactly("B", "C");
assertThat(email.getSubject()).isEqualTo("subject");
assertThat(email.getContent()).isEqualTo("content");
assertThat(email.getBcc()).map(EmailAddress::getAddress)
.singleElement()
.isEqualTo("D");
assertThat(email.getCc()).map(EmailAddress::getAddress)
.singleElement()
.isEqualTo("E");
}
10.3 柯里化
参考链接:Currying
百度百科对 柯里化 的解释是:在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。
给定函数 ,使用科里化得到新函数 , 从 中获取一个参数,然后返回一个将 映射到 的函数,因此有 。
以 JavaScript 为例,存在名为 add 的函数,接收三个参数,最终返回它们的和:
function add(a, b, c) {
return a + b + c
}
add(1, 2, 3) // 6
如果对 add 函数进行柯里化,那么有:
function curriedAdd(a) {
return function (b) {
return function (c) {
return a + b + c
}
}
}
curriedAdd(1)(2)(3) // 6
在 Java8 引入 Lambda 表达式后,Java 也能很轻松实现类似的功能。
比如现在需要构造一个 Book 对象,需要依次传入类型、作者、书名和出版时间等信息。一般来说,一个作者会出版许多同类型、不同名称的书籍,在构造这样的 Book 对象时,如果每次都传入相同的类型和作者信息会使得代码很冗余,为此可以参考柯里化编写出以下代码:
public enum Genre {
/**
* 奇幻
*/
FANTASY,
/**
* 恐怖
*/
HORROR,
/**
* 科幻
*/
SCI_FI,
}
public record Book(Genre genre, String author, String title, LocalDate publicationDate) {
public static AddGenre builder() {
return genre
-> author
-> title
-> publicationDate
-> new Book(genre, author, title, publicationDate);
}
public interface AddGenre {
Book.AddAuthor withGenre(Genre genre);
}
public interface AddAuthor {
Book.AddTitle withAuthor(String author);
}
public interface AddTitle {
Book.AddPublicationDate withTitle(String title);
}
public interface AddPublicationDate {
Book withPublicationDate(LocalDate publicationDate);
}
}
现在可以这样构造 Book 对象:
@Test
public void test() {
// Defining genre book functions
Book.AddAuthor fantasyBookFunc = Book.builder().withGenre(Genre.FANTASY);
Book.AddAuthor horrorBookFunc = Book.builder().withGenre(Genre.HORROR);
Book.AddAuthor scifiBookFunc = Book.builder().withGenre(Genre.SCI_FI);
// Defining author book functions
Book.AddTitle kingFantasyBooksFunc = fantasyBookFunc.withAuthor("Stephen King");
Book.AddTitle kingHorrorBooksFunc = horrorBookFunc.withAuthor("Stephen King");
Book.AddTitle rowlingFantasyBooksFunc = fantasyBookFunc.withAuthor("J.K. Rowling");
// Creates books by Stephen King (horror and fantasy genres)
Book shining = kingHorrorBooksFunc.withTitle("The Shining")
.withPublicationDate(LocalDate.of(1977, 1, 28));
Book darkTower = kingFantasyBooksFunc.withTitle("The Dark Tower: Gunslinger")
.withPublicationDate(LocalDate.of(1982, 6, 10));
// Creates fantasy books by J.K. Rowling
Book chamberOfSecrets = rowlingFantasyBooksFunc.withTitle("Harry Potter and the Chamber of Secrets")
.withPublicationDate(LocalDate.of(1998, 7, 2));
// Create sci-fi books
Book dune = scifiBookFunc.withAuthor("Frank Herbert")
.withTitle("Dune")
.withPublicationDate(LocalDate.of(1965, 8, 1));
Book foundation = scifiBookFunc.withAuthor("Isaac Asimov")
.withTitle("Foundation")
.withPublicationDate(LocalDate.of(1942, 5, 1));
// --snip--
}