Java 设计模式学习网站:Java设计模式:23种设计模式全面解析(超级详细)
知乎专栏:秒懂设计模式之建造者模式(Builder pattern)
《Effective Java 中文版(原书第 3 版)》 —— 俞黎敏译
1. 模式的定义与特点
参考链接:建造者模式(Bulider模式)详解
建造者(Builder)模式(又称构建者模式):将复杂对象的构造过程与其表示形式分离,使同一构造过程创建出不同的表示形式。它是将一个复杂的对象分解为多个简单的对象,然后一步一步构建而成。它将变与不变相分离,即产品的组成部分是不变的,但每一部分是可以灵活选择的。
优点
封装性好,构建和表示分离
扩展性好,各个具体的建造者相互独立,有利于系统的解耦
客户端不必知道产品内部组成的细节,建造者可以对创建过程逐步细化,而不对其它模块产生任何影响,便于控制细节风险。
缺点
产品的组成部分必须相同,这限制了其使用范围
如果产品的内部结构复杂,当产品内部发生变化时,建造者也要同步修改,后期维护成本较大
为了创建对象,必须先创建它的构建器,在十分注重性能的情况下,这是一笔性能开销
建造者(Builder)模式和工厂模式的关注点不同:建造者模式注重零部件的组装过程,而工厂方法模式更注重零部件的创建过程,但两者可以结合使用。
每个字都认识,但是合在一起就不理解了,这到底是个啥? 🤔
不明白没有关系,这段文字可记可不记,当然记住更好,以后和别人解释就有装逼的资格了。🤣
2. 适用场景
设计模式,最重要的是如何使用和适用场景。
在《Effective Java 中文版(原书第 3 版)》一书中的第 2 条就说到了:遇到多个构造器(Constructor)参数时要考虑使用构建器(Builder)。
说人话就是:如果一个类的构造函数参数有多个时,要考虑使用构造器(Builder)。
其实还是有点模糊,再说人话:当一个类的构造函数参数个数超过 4 个,而且某些参数是可选的参数,考虑使用建造者模式。
问题又来了:
建造者模式解决了哪些问题?
为什么要使用建造者模式?
怎么使用建造者模式?
3. 解决的问题
假设有这样一个 Person
类:
1 2 3 4 5 6 7 8 9 10 11 12 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 版)》一书中,首先提到了 重叠构造器模式 ,即创建一个包含需要设置的参数的构造函数。
1 2 3 4 5 6 7 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; }
但这种方式是很愚蠢的 😑
总之,采用 重叠构造器模式 来解决并不是一种好的方式。
3.2 JavaBeans 模式
在《Effective Java 中文版(原书第 3 版)》一书中,紧接着又提到了 JavaBeans 模式 。
简单来说:先调用一个无参构造函数来创建对象,然后使用 Setter 方法来设置必须与可选参数。
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 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 代码展示
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 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 ); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 @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
类
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 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; } 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; } }
抽象建造者类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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
类:
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 37 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; } }
指导者类
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Person2Director { public void makePerson2 (Person2Builder builder) { builder.setAge(); builder.setHeight(); } }
使用与测试
1 2 3 4 5 6 7 8 9 10 11 12 @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
的。如:
1 2 3 4 5 6 7 8 9 10 11 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; }
6. Lombok 中的 @Builder
6.1 基本使用
建造者模式的实现步骤都差不多,就像一个模板一样。
在 Lombok 中提供了 @Getter
和 @Setter
注解来解决冗杂 Getter 和 Setter 方法,它还提供了 @Builder
注解来一键生成建造者模式的代码。
创建一个新实体 Animal
,并使用 @Builder
注解:
1 2 3 4 5 6 7 8 9 10 11 @Builder @Getter public class Animal { private final String name; private final String type; private final Integer age; private final Integer gender; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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
后的内容:
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 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
:
1 @AllArgsConstructor(access = AccessLevel.PRIVATE)
最终的 Animal
类:
1 2 3 4 5 6 7 8 9 @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; }
重新编译后,生成的构造函数:
1 2 3 4 5 6 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
后虽然生成了建造者模式的结构,也可以实现属性值的选择性设置,但并没做到属性值的必须设置。要想实现也很简单只需要:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @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 方法设置一个新的名字。
然后自己编写一个方法,在这个方法中调用生成的方法,并设置必填值。
1 2 3 4 5 6 7 8 9 10 11 @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()); }
重新编译后,生产的部分内容为:
1 2 3 4 5 6 7 8 9 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
,表示披萨的各种馅料:
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 public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
用一个抽象类表示各种各样的披萨:
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 @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
,表示披萨的尺寸:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public enum Size { SMALL, MEDIUM, LARGE }
经典纽约风味的披萨的实现:
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 @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; } }
馅料内置的半月型披萨的实现:
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 37 @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; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @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
导致拷贝失败。
在这种情况下除了使用建造者模式外,还有其他的方法吗?
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 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; } }
测试一下:
1 2 3 4 5 6 7 8 9 @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
类:
1 2 3 4 5 6 7 8 9 @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 代码实现
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 @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 build () ; Build cc (List<EmailAddress> cc) ; 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 ; } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @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)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。
给定函数 f : ( X × Y ) → Z f \ : \ (X \times Y) \rightarrow Z f : ( X × Y ) → Z ,使用科里化得到新函数 h : X → ( Y → Z ) h \ : \ X \rightarrow (Y \rightarrow Z) h : X → ( Y → Z ) ,h h h 从 X X X 中获取一个参数,然后返回一个将 Y Y Y 映射到 Z Z Z 的函数,因此有 h ( x ) ( y ) = f ( x , y ) h(x)(y)=f(x,y) h ( x ) ( y ) = f ( x , y ) 。
以 JavaScript 为例,存在名为 add
的函数,接收三个参数,最终返回它们的和:
1 2 3 4 5 function add (a, b, c ) { return a + b + c } add (1 , 2 , 3 )
如果对 add
函数进行柯里化,那么有:
1 2 3 4 5 6 7 8 9 function curriedAdd (a ) { return function (b ) { return function (c ) { return a + b + c } } } curriedAdd (1 )(2 )(3 )
在 Java8 引入 Lambda 表达式后,Java 也能很轻松实现类似的功能。
比如现在需要构造一个 Book
对象,需要依次传入类型、作者、书名和出版时间等信息。一般来说,一个作者会出版许多同类型、不同名称的书籍,在构造这样的 Book
对象时,如果每次都传入相同的类型和作者信息会使得代码很冗余,为此可以参考柯里化编写出以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public enum Genre { FANTASY, HORROR, SCI_FI, }
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 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
对象:
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 @Test public void test () { 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); Book.AddTitle kingFantasyBooksFunc = fantasyBookFunc.withAuthor("Stephen King" ); Book.AddTitle kingHorrorBooksFunc = horrorBookFunc.withAuthor("Stephen King" ); Book.AddTitle rowlingFantasyBooksFunc = fantasyBookFunc.withAuthor("J.K. Rowling" ); 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 )); Book chamberOfSecrets = rowlingFantasyBooksFunc.withTitle("Harry Potter and the Chamber of Secrets" ) .withPublicationDate(LocalDate.of(1998 , 7 , 2 )); 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 )); }