Java 使用构建者模式创建对象实例

HYF Lv3

学习 Java 的小伙伴们都应该听说过 Java 四大名著。今天博主将与大家分享其中一本经典著作 《Effective Java》 中的一个实用知识点 —— 如何使用设计模式中的构建者模式( Builder Pattern )来优化具有大量参数的类的构造方法。

什么是构建者模式

首先我们需要了解一下什么是构建者模式。

构建者模式( Builder Pattern )是一种创建型设计模式,它允许你一步步创建复杂对象。与直接构造对象相比,使用构建者模式可以更灵活、更清晰地创建对象,尤其是当对象包含多个可选参数或复杂的构建过程时。
构建者模式的优点

开发中我们可能遇到的痛点

在日常的 Java 开发中,我们可能遇到一些业务需要创建一些具有多个参数的实体类。这些类可能代表某个复杂的对象,例如各种 VO DTO 、或者其他业务实体。当我们面对这些多参数实体类时,往往会遇到以下几个常见的痛点:

构造方法过多

当一个类有多个可选参数时,为了提供灵活的对象创建方式,我们可能会定义多个构造方法。这些构造方法根据不同的参数组合进行重载,最终导致代码冗长且维护困难。例如:

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
/**
* @author -侑枫
* @date 2024/7/17 0:07:01
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class UserDTO {

private static final long serialVersionUID = 1L;
private String id;
private String loginName;
private String password;
private String name;
private int age;
private String email;
private String phone;

public UserDTO(String id) {
this.id = id;
}

public UserDTO(String loginName, String password) {
this.loginName = loginName;
this.password = password;
}

public UserDTO(String id, String loginName, String password) {
this.id = id;
this.loginName = loginName;
this.password = password;
}

public UserDTO(String id, String loginName, String password, String email) {
this.id = id;
this.loginName = loginName;
this.password = password;
this.email = email;
}

public UserDTO(String id, String loginName, String password, String email, int age) {
this.id = id;
this.loginName = loginName;
this.password = password;
this.email = email;
this.age = age;
}

// ...可能创建的构造器太多,就不一一细写了
}
以上述代码举例当我们的参数达到 7 个时,我们可能出现的无参、单参、多参构造器总共有 11700 种(此处应为使用排列数而非组合数,所以应该是P(n,k),而非前文的 128 种,感谢大佬纠正,hhhh)。其中可能常用的几种或者几十种,如果一一写在类中,代码构造方法的数量爆炸且及其难以管理。

创建对象赋值不灵活

有同学可能会说了:”我们可以使用 Lombok 给我们自动生成一个全参构造器(使用 @AllArgsConstructor )和无参构造器(使用 @NoArgsConstructor )再一一赋值不就好了。“

再次以上述代码举例,我们将上述两个注解添加到我们的 DTO 类种。假设我现在需要创建一个只有 loginName 张三 phone 1008611 的对象,以及一个拥有全部参数的对象:
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
public static void main(String[] args) {
/**
* 创建一个只有 `loginName` 和 `phone` 的对象:方式一: 全参构造器
*/
UserDTO userDTO = new UserDTO("", "张三", "", "", 0, "", "1008611");

/**
* 创建一个只有 `loginName` 和 `phone` 的对象:方式二: 无参构造器
*/
UserDTO userDTO1 = new UserDTO();
userDTO1.setLoginName("张三");
userDTO1.setPassword("1008611");


/**
* 创建一个拥有全部参数的对象: 方式一: 全参构造器
*/
UserDTO userDTO2 = new UserDTO("1", "张三", "loginName", "zhangsan", 18, "[email protected]", "1008611");

/**
* 创建一个拥有全部参数的对象: 方式二: 无参构造器
*/
UserDTO userDTO3 = new UserDTO();
userDTO3.setId("2");
userDTO3.setName("zhangsan");
userDTO3.setEmail("[email protected]");
userDTO3.setLoginName("password");
userDTO3.setAge(18);
userDTO3.setPhone("1008611");

}

用设计模式中的构建者模式思想优化痛点

为了优化具有大量参数的类的构造方法并解决前述的痛点, 《Effective Java》 中推荐使用构建者模式( Builder Pattern )。构建者模式通过引入一个辅助的构建者类,将对象的创建过程分解成一系列可控的步骤,从而提高代码的可读性、可维护性和灵活性。

构建者模式的基本思想

构建者模式的核心思想是将对象的创建过程与其表示分离。具体来说,构建者模式通过一个静态内部类( Builder ),提供一系列方法来设置对象的各个属性,并最终通过一个 build() 方法创建并返回目标对象。

优化多参数实体类的步骤

还是以上述实体类举例说明,以下是使用构建者模式优化多参数实体类的具体步骤:

定义静态内部类 Builder

我们先把每一个参数加上 final 关键字(也可以不加,但是加了更符合面向对象设计原则),构造器全部清掉。接着在目标类中定义一个静态内部类 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
/**
* @author -侑枫
* @date 2024/7/17 0:07:01
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class UserDTO implements Serializable {

private final static final long serialVersionUID = 1L;
private final String id;
private final String loginName;
private final String password;
private final String name;
private final int age;
private final String email;
private final String phone;

public static class Builder {

/**
* 如果希望该字段为必填,那么加上关键字 final
*/
private final String id;
private String loginName;
private String password;
private String name;
private int age;
private String email;
private String phone;
}
}

创建 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
/**
* @author -侑枫
* @date 2024/7/17 0:07:01
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class UserDTO implements Serializable {

private final static final long serialVersionUID = 1L;
private final String id;
private final String loginName;
private final String password;
private final String name;
private final int age;
private final String email;
private final String phone;

public static class Builder {

/**
* 如果该字段为必填,那么加上关键字 final
*/
private final String id;
private String loginName;
private String password;
private String name;
private int age;
private String email;
private String phone;

// 例如我的类中只有 id 为必须要的参数(当然这个构造方法也可以不要)
public Builder(String id) {
this.id = id;
}
}
}

定义可选参数的 setter 方法

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
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
/**
* @author -侑枫
* @date 2024/7/17 0:07:01
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class UserDTO implements Serializable {

private static final long serialVersionUID = 1L;
private final String id;
private final String loginName;
private final String password;
private final String name;
private final int age;
private final String email;
private final String phone;

public static class Builder {

/**
* 如果该字段为必填,那么加上关键字 final
*/
private final String id;
private String loginName;
private String password;
private String name;
private int age;
private String email;
private String phone;

// 例如我的类中只有 id 为必须要的参数(当然这个构造方法也可以不要,以下一小节要讲的 setter 方法代替)
public Builder(String id) {
this.id = id;
}

public Builder loginName(String val) {
loginName = val;
return this;
}

public Builder password(String val) {
password = val;
return this;
}

public Builder name(String val) {
name = val;
return this;
}

public Builder age(int val) {
age = val;
return this;
}

public Builder email(String val) {
email = val;
return this;
}

public Builder phone(String val) {
phone = val;
return this;
}

}
}

定义 build() 方法

Builder 类中,定义一个 build() 方法,该方法创建并返回目标对象。在 build() 方法中,通过将 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
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
/**
* @author -侑枫
* @date 2024/7/17 0:07:01
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class UserDTO implements Serializable {

private static final long serialVersionUID = 1L;
private final String id;
private final String loginName;
private final String password;
private final String name;
private final int age;
private final String email;
private final String phone;

public static class Builder {

/**
* 如果该字段为必填,那么加上关键字 final
*/
private final String id;
private String loginName;
private String password;
private String name;
private int age;
private String email;
private String phone;

// 例如我的类中只有 id 为必须要的参数(当然这个构造方法也可以不要,以下一小节要讲的 setter 方法代替)
public Builder(String id) {
this.id = id;
}

public Builder loginName(String val) {
loginName = val;
return this;
}

public Builder password(String val) {
password = val;
return this;
}

public Builder name(String val) {
name = val;
return this;
}

public Builder age(int val) {
age = val;
return this;
}

public Builder email(String val) {
email = val;
return this;
}

public Builder phone(String val) {
phone = val;
return this;
}

public UserDTO build() {
return new UserDTO(this);
}

}
}

(细心的同学敲到这里会发现编译出现报错,别紧张,也不是你操作错误了,我们接着下一步)

在目标类中定义私有构造方法

在目标类中,定义一个私有的构造方法,该方法接受一个 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
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
/**
* @author -侑枫
* @date 2024/7/17 0:07:01
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class UserDTO implements Serializable {

private static final long serialVersionUID = 1L;
private final String id;
private final String loginName;
private final String password;
private final String name;
private final int age;
private final String email;
private final String phone;

private UserDTO(Builder builder) {
this.id = builder.id;
this.loginName = builder.loginName;
this.password = builder.password;
this.name = builder.name;
this.age = builder.age;
this.email = builder.email;
this.phone = builder.phone;
}

public static class Builder {

/**
* 如果该字段为必填,那么加上关键字 final
*/
private final String id;
private String loginName;
private String password;
private String name;
private int age;
private String email;
private String phone;

// 例如我的类中只有 id 为必须要的参数(当然这个构造方法也可以不要,以下一小节要讲的 setter 方法代替)
public Builder(String id) {
this.id = id;
}

public Builder loginName(String val) {
loginName = val;
return this;
}

public Builder password(String val) {
password = val;
return this;
}

public Builder name(String val) {
name = val;
return this;
}

public Builder age(int val) {
age = val;
return this;
}

public Builder email(String val) {
email = val;
return this;
}

public Builder phone(String val) {
phone = val;
return this;
}

public UserDTO build() {
return new UserDTO(this);
}

}
}

可以看到刚刚报编译错误的地方已经消失了,而我们的构建者模式也已经创建好了

构建者模式优化了什么?

使用示例

以下是使用构建者模式优化具有 7 个参数的 UserDTO 类的示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
/**
* 创建一个id为123,loginName为zhangsan,name为张三,password为password的DTO。(为什么不展示全部参数?不想写了 太长了,懒)
* 如果我们刚刚没有定义一个带有所有必需参数的构造方法,则 Builder(id) 中的 id 可以放空
* 例如: UserDTO userDTO = new UserDTO.Builder().loginName("zhangsan")。。。。。。
*/
UserDTO userDTO = new UserDTO.Builder("123")
.loginName("zhangsan")
.name("张三")
.password("password")
.build();
}

看到这里有的同学又说了:“ 什么狗屎构建者模式,一个类要写这么多东西,我代码生成器生成的怎么办?这跟我写几个常用构造方法有什么区别? ” 别急,我们刚刚用到的一个神器又排上用场了,他就是 Lombox

使用 Lombox 简化构建者模式

手动编写构建器类繁琐,且很不优雅,而 Lombox 的注解 @Builder 可以很贴心地帮我们实现这个需求。

还是以上述实体类代码举例,我们将刚刚手写的构建器类、私有构造器删除,并加上我们的 @Builder 注解:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @author -侑枫
* @date 2024/7/17 0:07:01
*/
@Data
@Builder
@EqualsAndHashCode(callSuper = false)
public class UserDTO implements Serializable {

private static final long serialVersionUID = 1L;
private final String id;
private final String loginName;
private final String password;
private final String name;
private final int age;
private final String email;
private final String phone;
}

如此,便完成了我们的构建器的创建,有同学可能又要说了:“ 有这么好用的东西不早拿出来,讲那么多废话,害我看了那么久博客,我**你个**

害, 这不都是为了水文字嘛 这不都是为了让大家知道这个注解的原理、或者说它做了哪些事情嘛,我们要学会造轮子(有用的轮子)而不是用轮子(而且本文想讲述的是构建者模式的一种实现,上来就讲注解也不是个事阿~~)。

咳咳,题外话哈,接下来我们来演示一下使用这个 @Builder 注解之后,我们该如何使用构建者模式来创建类(跟我们手写的稍有差别):

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
/**
* 创建一个id为123,loginName为zhangsan,name为张三,password为password的DTO。(为什么不展示全部参数?不想写了 太长了,懒)
* 如果我们刚刚没有定义一个带有所有必需参数的构造方法,则 Builder(id) 中的 id 可以放空
* 例如: UserDTO userDTO = new UserDTO.Builder().loginName("zhangsan")。。。。。。
*/
UserDTO userDTO = UserDTO.builder()
.id("123")
.loginName("zhangsan")
.name("张三")
.password("password")
.build();
}
可以看到,使用 Lombok @Builder 注解可以大幅减少样板代码,提高开发效率,适用于大多数情况下的构建者模式实现。在使用时我们不需要再去 new 一个对象(阿?我对象呢??噢~我本来就没对象),而是直接调用它自动生成静态的 builder() 方法,十分的银杏化。

Lombok @Builder 注解提供了几个可选参数来定制构建器的行为。我们来介绍一下其中 2 个比较常用的:

例如,我们在 @Builder 注解中加上 toBuilder = true 参数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @author -侑枫
* @date 2024/7/17 0:07:01
*/
@Data
@Builder(toBuilder = true)
@EqualsAndHashCode(callSuper = false)
public class UserDTO implements Serializable {

private static final long serialVersionUID = 1L;
private final String id;
private final String loginName;
private final String password;
private final String name;
private final int age;
private final String email;
private final String phone;
}
使用方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
/**
* 创建一个id为123,loginName为zhangsan,name为张三,password为password的DTO。(为什么不展示全部参数?不想写了 太长了,懒)
* 如果我们刚刚没有定义一个带有所有必需参数的构造方法,则 Builder(id) 中的 id 可以放空
* 例如: UserDTO userDTO = new UserDTO.Builder().loginName("zhangsan")。。。。。。
*/
UserDTO userDTO = UserDTO.builder()
.id("123")
.loginName("zhangsan")
.name("张三")
.password("password")
.build();

// 更新了 name,其他属性保持不变
UserDTO newUserDTO = userDTO.toBuilder().name("李四盗号了").build();
}
例如,使用 builderMethodName 自定义构建器方法名:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @author -侑枫
* @date 2024/7/17 0:07:01
*/
@Data
@Builder(toBuilder = true, builderMethodName = "myBuilder")
@EqualsAndHashCode(callSuper = false)
public class UserDTO implements Serializable {

private static final long serialVersionUID = 1L;
private final String id;
private final String loginName;
private final String password;
private final String name;
private final int age;
private final String email;
private final String phone;
}
使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
/**
* 创建一个id为123,loginName为zhangsan,name为张三,password为password的DTO。(为什么不展示全部参数?不想写了 太长了,懒)
* 如果我们刚刚没有定义一个带有所有必需参数的构造方法,则 Builder(id) 中的 id 可以放空
* 例如: UserDTO userDTO = new UserDTO.Builder().loginName("zhangsan")。。。。。。
*
* 原 builder 改为 myBuilder
*/
UserDTO userDTO = UserDTO.myBuilder()
.id("123")
.loginName("zhangsan")
.name("张三")
.password("password")
.build();

// 更新了 name,其他属性保持不变
UserDTO newUserDTO = userDTO.toBuilder().name("李四盗号了").build();
}

结语

通过本文的学习,我们深入探讨了 《Effective Java》 中的构建者模式( Builder Pattern )以及如何使用它来优化具有大量参数的类的构造方法。构建者模式通过引入一个独立的构建者类,将复杂对象的创建过程分解成一系列可控的步骤,从而提高了代码的可读性、可维护性和灵活性。

在实现方法上,我们使用了手动编写构建者模式和 Lombok @Builder 注解两种方式。手动编写可以提供更高的灵活性和控制,适合特定需求和复杂场景;而使用 Lombok 简化了大部分样板代码,提高了开发效率,适合大部分的 Builder 模式实现。

无论是哪种方式,构建者模式的应用都能有效地解决多参数构造方法带来的痛点,使得对象的创建过程更加清晰和安全。在实际开发中,根据项目需求和团队技术栈的选择,灵活运用构建者模式,将有助于提升代码质量和开发效率。

希望本文能为你在使用构建者模式时提供一些指导和帮助,欢迎分享你的想法和经验,一起探讨如何更好地利用设计模式来提升 Java 开发的水平和效率。