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

HYF Lv3

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

什么是构建者模式

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

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

该设计模式主要有以下角色

产品类(Product):需要被构建的复杂对象,通常具有多个属性。

抽象构建者类(Builder):定义了构建产品对象所需的所有方法和属性。通常包括设置产品的各个属性的方法,以及构建产品的方法。

具体构建者类(Concrete Builder):实现了抽象构建者类定义的方法,并且返回一个组装好的产品实例。

指导者类(Director):负责使用构建者类中的方法来构建最终的对象。它并不关心具体的构建过程,只负责按照一定的顺序调用构建者的方法来构建对象。

构建者模式的优点

可以控制产品对象的创建过程和细节。

可以更加灵活地创建不同的产品配置。

可以通过链式调用使得代码的可读性和可维护性更好。

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

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

构造方法过多

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

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张三phone1008611 的对象,以及一个拥有全部参数的对象:
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, "1008611@gmail.com", "1008611");

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

}

使用全参构造器创建类

大家如果遇到过类似手动输入全参构造器的方式可以发现,在多参的情况下,容易出现参数顺序错误的问题。参数顺序错误是一个常见的陷阱,尤其是在参数类型相同的情况下。

当一个类的构造方法包含大量参数时,即使参数顺序正确,代码的可读性也会大幅降低。调用者很难一眼看出各个参数的含义,需要频繁地查看方法签名才能弄清楚每个参数的作用。

有时,我们需要为某些参数赋值。在使用多参数构造方法时,赋值变得非常复杂且不直观。使用多个构造方法来覆盖不同的参数组合虽然能部分解决这个问题,但会导致代码冗余和维护困难。

使用无参构造器创建类

在需要赋值参数较少的情况下,确实能解决全参构造器的一些痛点。但如果参数过多,每一个参数都使用 setter 方法赋值可能会使你的代码量爆炸性增长

由于无参构造器无法保证所有必需属性都已设置,开发者可能会忘记调用相应的 setter 方法,导致对象处于错误赋值、不完整或重复赋值的状态。这种错误在运行时才会显现,增加了调试的难度。

无参构造器通常需要依赖 setter 方法或其他初始化方法来设置对象的状态。这种做法使得代码的意图不够明确,难以一眼看出对象创建时需要哪些必需的属性,降低了代码的可读性。

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

为了优化具有大量参数的类的构造方法并解决前述的痛点,《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);
}

}
}

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

构建者模式优化了什么?

避免构造方法过多:通过 Builder 类的方法来设置参数,避免了传统多参数构造方法的数量爆炸问题,使得代码更加简洁和易于维护。

消除参数顺序错误:每个参数都有专门的 setter 方法,调用者可以任意顺序设置参数,避免了参数顺序错误的问题。

提高可读性Builder 类的方法名明确表示了每个参数的含义,链式调用使代码更加直观和易读。

处理默认值:在 Builder 类中,可选参数可以有默认值,调用者只需设置需要的参数,无需关心默认值的处理。

保持不可变性:构建后的对象是不可变的,所有字段都是 final,确保对象一旦创建,其内部状态不能被修改,符合良好的面向对象设计原则。

使用示例

以下是使用构建者模式优化具有 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 个比较常用的:

toBuilder = true:

toBuilder = true 被设置时,Lombok 会为类生成一个 toBuilder() 方法。这个方法允许你从现有的对象创建一个新的构建器实例,从而可以修改对象的部分属性而不影响原始对象。这对于更新现有对象的某些属性特别有用,因为你可以基于当前对象的状态创建一个新的对象,而不需要重复输入所有不变的属性值。

builderMethodName = "myBuilder"

这个参数允许你自定义构建器方法的名字。默认情况下,Lombok 生成的构建器方法名为 builder()。如果你希望构建器方法有不同于默认的名字,比如 myBuilder(), 可以通过设置 builderMethodName 来实现。这在需要避免命名冲突或者有特殊命名约定的项目中很有用。

例如,我们在 @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 开发的水平和效率。

  • 标题: Java 使用构建者模式创建对象实例
  • 作者: HYF
  • 创建于 : 2024-07-16 23:53:36
  • 更新于 : 2024-07-27 21:21:52
  • 链接: https://youfeng.ink/Builder-Pattern-c69f90740a6e/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。