学习 Java
的小伙伴们都应该听说过 Java
四大名著。今天博主将与大家分享其中一本经典著作《Effective Java》
中的一个实用知识点 —— 如何使用设计模式中的构建者模式(Builder Pattern
)来优化具有大量参数的类的构造方法。
什么是构建者模式
首先我们需要了解一下什么是构建者模式。
构建者模式(Builder Pattern
)是一种创建型设计模式,它允许你一步步创建复杂对象。与直接构造对象相比,使用构建者模式可以更灵活、更清晰地创建对象,尤其是当对象包含多个可选参数或复杂的构建过程时。
产品类(Product):需要被构建的复杂对象,通常具有多个属性。
抽象构建者类(Builder):定义了构建产品对象所需的所有方法和属性。通常包括设置产品的各个属性的方法,以及构建产品的方法。
具体构建者类(Concrete Builder):实现了抽象构建者类定义的方法,并且返回一个组装好的产品实例。
指导者类(Director):负责使用构建者类中的方法来构建最终的对象。它并不关心具体的构建过程,只负责按照一定的顺序调用构建者的方法来构建对象。
构建者模式的优点
开发中我们可能遇到的痛点
在日常的 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
|
@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) {
UserDTO userDTO = new UserDTO("", "张三", "", "", 0, "", "1008611");
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
|
@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 {
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
|
@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 {
private final String id; private String loginName; private String password; private String name; private int age; private String email; private String phone;
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
|
@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 {
private final String id; private String loginName; private String password; private String name; private int age; private String email; private String phone;
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
|
@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 {
private final String id; private String loginName; private String password; private String name; private int age; private String email; private String phone;
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
|
@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 {
private final String id; private String loginName; private String password; private String name; private int age; private String email; private String phone;
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) {
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
|
@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) {
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
|
@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) {
UserDTO userDTO = UserDTO.builder() .id("123") .loginName("zhangsan") .name("张三") .password("password") .build(); 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
|
@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) {
UserDTO userDTO = UserDTO.myBuilder() .id("123") .loginName("zhangsan") .name("张三") .password("password") .build();
UserDTO newUserDTO = userDTO.toBuilder().name("李四盗号了").build(); }
|
结语
通过本文的学习,我们深入探讨了 《Effective Java》
中的构建者模式(Builder Pattern
)以及如何使用它来优化具有大量参数的类的构造方法。构建者模式通过引入一个独立的构建者类,将复杂对象的创建过程分解成一系列可控的步骤,从而提高了代码的可读性、可维护性和灵活性。
在实现方法上,我们使用了手动编写构建者模式和 Lombok
的 @Builder
注解两种方式。手动编写可以提供更高的灵活性和控制,适合特定需求和复杂场景;而使用 Lombok
简化了大部分样板代码,提高了开发效率,适合大部分的 Builder
模式实现。
无论是哪种方式,构建者模式的应用都能有效地解决多参数构造方法带来的痛点,使得对象的创建过程更加清晰和安全。在实际开发中,根据项目需求和团队技术栈的选择,灵活运用构建者模式,将有助于提升代码质量和开发效率。
希望本文能为你在使用构建者模式时提供一些指导和帮助,欢迎分享你的想法和经验,一起探讨如何更好地利用设计模式来提升 Java
开发的水平和效率。