疯狂的类构造器 最近栈长在做 Code Review 时,发现一段创建对象的方法:
Task task = new Task(112, "紧急任务" , "处理一下这个任务" , 90, 3, 1, 36, "刘主管" , 18, "客服1" , "11, 12, 13" , "客服3, 客服4, 客服5" , true , new Date(), new Date(), new Date(), new Date(), new Date(), 0, "需要尽快完成" , ...);
真实代码敏感性,上面的代码仅为模仿,实际要比这个更长、更复杂……
当我看到那段代码时,我简直要疯了!!
拖了半天才看完,到处充满着魔法值不说,把一个类所有参数都放在一个构造器里面,这个构造器也太疯狂了……这种写法也实在太 low 了!
在实际开发过程中,栈长经常看到同事们这样的写法,比上面的更长的构造器你见过没?我反正见过!
一方面,也许他们真不知道怎么写才更好,毕竟经验有限,这个可以原谅。但另一方面,也许他们就是为了偷懒,或者为了赶时间,反正这都是对自己和同事不负责的表现。
如果你在公司看到同事写这样的优秀代码,请把这篇文章发给他。
看看大量参数构造器的缺点:
不能判断出哪些是必须参数,哪些是可选参数,可选参数也得给个默认值; 分不清变量值对应哪个变量,如顺序对应错,很容易造成错误; 构造器参数增减时,会影响所有创建该对象的地方,影响扩展性; 构造器正确的用法是只给出几个必选、重要参数的构造器,而不是把所有参数放在一个构造器中。
比如我们看下 JDK 线程池的构造器用法:
线程池就把几个重要的参数封装成了几个构造器,这样用户就可以根据实际需要调用具体的某个构造器。
基本 SET 方法改良 再回到同事写的那个代码,写出那样长的构造器,我真的服了。
最基本也得写成这样吧:
Task task = new Task(); task.setId(112); task.setName("紧急任务" ); task.setContent("处理一下这个任务" ); task.setParentId(90); task.setType(3); task.setLevel(1); task.setAssignFromId(36); task.setAssignFromName("刘主管" ); task.setAssignTo(18); task.setAssignTo("客服1" ); task.setCandidateId("11, 12, 13" ); task.setCandidateName("客服3, 客服4, 客服5" ); task.isSendEmail(true ); task.setCreateTime(new Date()); task.setBeginTime(new Date()); task.setEndTime(new Date()); task.setFinishTime(new Date()); task.setUpdateTime(new Date()); task.setStatus(0); task.setMemo("需要尽快完成" ); // ...
这个创建对象的方式是最普通不过了,也是用的最多的了。
这种写法虽然看起来很直观,但是有以下几个缺点:
参数多的情况下 setter 非常多,代码非常长,不是很优雅; 容易搞错对象名,造成潜在错误,很难排查(如:同时有 user 和 user2,在 user 赋值过程中错误的复制了 user2 对象); Builder 模式改良 下面栈长教大家用 Builder 模式改良下,下面是改良后的代码:
package cn.javastack.test.designpattern.builder; import java.util.Date; /** * @author: 栈长 * @from: 公众号Java技术栈 */ public class Task { private long id; private String name; private String content; private int type ; private int status; private Date finishDate; private Task(TaskBuilder taskBuilder) { this.id = taskBuilder.id; this.name = taskBuilder.name; this.content = taskBuilder.content; this.type = taskBuilder.type; this.status = taskBuilder.status; this.finishDate = taskBuilder.finishDate; } /** * @author: 栈长 * @from: 公众号Java技术栈 */ public static class TaskBuilder { private long id; private String name; private String content; private int type ; private int status; private Date finishDate; public TaskBuilder(long id, String name) { this.id = id; this.name = name; } public TaskBuilder content(String content) { this.content = content; return this; } public TaskBuilder type (int type ) { this.type = type ; return this; } public TaskBuilder status(int status) { this.status = status; return this; } public TaskBuilder finishDate(Date finishDate) { this.finishDate = finishDate; return this; } public Task build (){ return new Task(this); } } public long getId () { return id; } public String getName () { return name; } public String getContent () { return content; } public int getType () { return type ; } public int getStatus () { return status; } public Date getFinishDate () { return finishDate; } @Override public String toString () { return "Task{" + "id=" + id + ", name='" + name + '\' ' + ", content=' " + content + '\'' + " , type =" + type + " , status=" + status + " , finishDate=" + finishDate + '}'; } }
说下简单思路:
1)在 Bean 类里面新建一个静态内部类:XxxBuilder;
2)把 Bean 类所有参数复制到 XxxBuilder,然后在 XxxBuilder 新建必须参数的构造器,其他参数使用变量名作为方法然后返回自身(this)以便形成链式调用;
3)在 Bean 类里面新建一个接收 XxxBuilder 参数的私有构造器,避免使用 new 创建对象;
4)在 XxxBuilder 类新建一个 build 方法开始构建 Bean 类,也是作为链式调用的结束;
使用方法:
使用方式如下,先创建构造器,然后在每个方法后使用 . 带出所有方法,一目了然,最后调用 build 方法以结束链式调用创建 bean。
参考代码如下:
/** * @author: 栈长 * @from: 公众号Java技术栈 */ private static void testBuilder () { Task task = new Task.TaskBuilder(99, "紧急任务" ) .type (1) .content("处理一下这个任务" ) .status(0) .finishDate(new Date()) .build(); System.out.println(task); }
结果输出:
Task{id=99, name='紧急任务', content='处理一下这个任务', type=1, status=0, finishDate=...
Builder 模式的优点:
一行代码完成对象创建,避免了多行代码赋值过程出错; Builder 模式的缺点:
需要冗余的 Builder 类,以及大量相等重复的成员变量,大大增加了代码量,维护难度相对较大; 只适合一次赋值创建对象,多次赋值的场景还需要新增 set 方法配合,不是很灵活; Lombok 实现 Builder 模式 常规的 Builder 模式需要新增大量的代码,维护难度比较大,这里栈长再介绍一下 Lombok 中的 Builder 模式,一个 @Builder 注解搞定所有,轻松维护。
用 Lombok 改良后的代码如下:
/** * @author: 栈长 * @from: 公众号Java技术栈 */ @Builder public class LombokTask { private long id; private String name; private String content; private int type ; private int status; private Date finishDate; }
我还能说什么?两个字:真香!
再来看下怎么使用:
/** * @author: 栈长 * @from: 公众号Java技术栈 */ private static void testLombokBuilder () { LombokTask lombokTask = new LombokTask.LombokTaskBuilder() .id(99) .name("紧急任务" ) .type (1) .content("处理一下这个任务" ) .status(0) .finishDate(new Date()) .build(); System.out.println(lombokTask); }
或者 new 都不要了,直接调用静态方法:
/** * @author: 栈长 * @from: 公众号Java技术栈 */ private static void testLombokBuilder2 () { LombokTask lombokTask = LombokTask.builder() .id(99) .name("紧急任务" ) .type (1) .content("处理一下这个任务" ) .status(0) .finishDate(new Date()) .build(); System.out.println(lombokTask); }
接下来我们来看下这个 @Builder 注解到底做了什么:
public class LombokTask { private long id; private String name; private String content; private int type ; private int status; private Date finishDate; LombokTask(final long id, final String name, final String content, final int type , final int status, final Date finishDate) { this.id = id; this.name = name; this.content = content; this.type = type ; this.status = status; this.finishDate = finishDate; } public static LombokTask.LombokTaskBuilder builder () { return new LombokTask.LombokTaskBuilder(); } public static class LombokTaskBuilder { private long id; private String name; private String content; private int type ; private int status; private Date finishDate; LombokTaskBuilder () { } public LombokTask.LombokTaskBuilder id(final long id) { this.id = id; return this; } public LombokTask.LombokTaskBuilder name(final String name) { this.name = name; return this; } public LombokTask.LombokTaskBuilder content(final String content) { this.content = content; return this; } public LombokTask.LombokTaskBuilder type (final int type ) { this.type = type ; return this; } public LombokTask.LombokTaskBuilder status(final int status) { this.status = status; return this; } public LombokTask.LombokTaskBuilder finishDate(final Date finishDate) { this.finishDate = finishDate; return this; } public LombokTask build () { return new LombokTask(this.id, this.name, this.content, this.type, this.status, this.finishDate); } public String toString () { return "LombokTask.LombokTaskBuilder(id=" + this.id + ", name=" + this.name + ", content=" + this.content + ", type=" + this.type + ", status=" + this.status + ", finishDate=" + this.finishDate + ")" ; } } }
这是反编译后的代码,可以看出来逻辑都是一样的。
Lombok 还可以添加各种类构造器、toString 等系列注解,几个注解完全可以达到想要的效果,但代码量和可维护性是天壤之别。
很多人不建议使用 Lombok,仁者见仁,智者见智,这里不再讨论,相关话题可以阅读我之前写的文章:
使用 Lombok 带来了很多便利,不用多说,是真的香,东西是好东西,就是要团队规范一起使用,避免踩坑。更多工具系列使用文章请关注公众号Java技术栈,在菜单中阅读。
Java 8 实现 Builder 模式 Java 8 带来了函数式接口编程,所以在 Java 8 中可以一个实现通用的 Builder:
public class GenericBuilder <T > { private final Supplier<T> instantiator; private List<Consumer<T>> instanceModifiers = new ArrayList<>(); public GenericBuilder (Supplier<T> instantiator) { this .instantiator = instantiator; } public static <T> GenericBuilder<T> of (Supplier<T> instantiator) { return new GenericBuilder<T>(instantiator); } public <U> GenericBuilder<T> with (BiConsumer<T, U> consumer, U value) { Consumer<T> c = instance -> consumer.accept(instance, value); instanceModifiers.add(c); return this ; } public T build () { T value = instantiator.get(); instanceModifiers.forEach(modifier -> modifier.accept(value)); instanceModifiers.clear(); return value; } }
参考:
http://www./java8-builder.html
使用方式:
/** * @author: 栈长 * @from: 公众号Java技术栈 */ private static void testJava8Builder () { Java8Task java8Task = GenericBuilder.of(Java8Task::new) .with(Java8Task::setId, 99L) .with(Java8Task::setName, "紧急任务" ) .with(Java8Task::setType, 1) .with(Java8Task::setContent, "处理一下这个任务" ) .with(Java8Task::setStatus, 0) .with(Java8Task::setFinishDate, new Date()) .build(); System.out.println(java8Task); }
这样一来,任何带有默认构造器和 set 方法的类都可以使用这个通用的 Builder 模式了。
虽然利用 Java 8 是实现了通用有 Builder 模式,但还是有很多冗余的代码,而且本质还是调用的 set 方法,所以和 set 比起来只是多了一个链式调用而已。
Spring Boot 中的 Builder 模式 Spring Boot 是现在主流的应用框架,其中也用到了 Builder 模式,可见 Builder 模式的常见性。
下面再来看下 Spring Boot 是怎么应用 Builder 模式的:
new SpringApplicationBuilder() .sources(Parent.class) .child(Application.class) .bannerMode(Banner.Mode.OFF) .run(args);
如上代码所示,这是 Spring Boot 的链式启动方式。
Spring Boot 基础教程看这里:
https://github.com/javastacks/spring-boot-best-practice
我们来看它是怎么做的:
它是新增了一个 XxxBuilder 类:SpringApplicationBuilder,然后在 SpringApplicationBuilder 中新增了个 SpringApplication 的成员变量,然后再新增变量对应的方法。
所以,Spring Boot 只是用 SpringApplicationBuilder 包装了一下 SpringApplication 而已,写法有所不同,但中心思想都是一样的。这里就不再演示了,大家也可以了借鉴一下。
总结 本文说了同事写的疯狂的类构造器,然后再介绍了用 set 方法改良,以及使用 4 种 Builder 模式改良的方式,下面来总结一下吧:
Spring Boot 中的 Builder 模式 如果团队有使用 Lombok,那么 Lombok 无疑是最佳推荐的方式,代码量少,使用简单,方便维护。其他三种实现方式都各有利弊,大家都可以参考使用。
总之,别再写疯狂的类构造器了……
如果你在公司看到同事写这样的优秀代码,请把这篇文章发给他。
好了,今天的分享就到这里了,后面栈长我会更新更多 Java 技术实战及设计模式系列文章,公众号Java技术栈第一时间推送。
本节教程所有实战源码已上传到这个仓库:
https://github.com/javastacks/javastack
最后,觉得我的文章对你用收获的话,动动小手,给个在看、转发,原创不易,栈长需要你的鼓励。