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 /**
* @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 版)》一书中,首先提到了 重叠构造器模式 ,即创建一个包含需要设置的参数的构造函数。
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 /**
* @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 );
}
}
}
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 /**
* @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 方法
}
抽象建造者类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 /**
* @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 类:
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 /**
* @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;
}
}
指导者类
1 2 3 4 5 6 7 8 9 10 11 12 13 /**
* @author mofan 2021/2/3
*/
public class Person2Director {
public void makePerson2 ( Person2Builder builder ) {
/*
* 要为目标实体设置那些可选参数就在此设置
* 比如,我只想让我的目标实体具有 age 和 height 两个可选属性值
*/
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 ;
// --snip--
}
6. Lombok 中的 @Builder
6.1 基本使用
建造者模式的实现步骤都差不多,就像一个模板一样。
在 Lombok 中提供了 @Getter 和 @Setter 注解来解决冗杂 Getter 和 Setter 方法,它还提供了 @Builder 注解来一键生成建造者模式的代码。
创建一个新实体 Animal,并使用 @Builder 注解:
1 2 3 4 5 6 7 8 9 10 11 /**
* @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 ;
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 /**
* @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 后的内容:
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 /**
* @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 方法设置一个新的名字。
然后自己编写一个方法,在这个方法中调用生成的方法,并设置必填值。
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 /**
* @author mofan
* @date 2022/10/5 12:34
*/
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 /**
* @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,表示披萨的尺寸:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 /**
* @author mofan
* @date 2022/10/5 13:21
*/
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 /**
* @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 ;
}
}
馅料内置的半月型披萨的实现:
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 /**
* @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 ;
}
}
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 /**
* 链式调用的新思路
* 我并不认为这是建造者模式的通用写法
*
* @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;
}
}
测试一下:
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 /**
* @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 ;
}
}
}
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 ) // 6
如果对 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 ) // 6
在 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 () {
// 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--
}