分享

同事写了一个疯狂的类构造器,我要疯了,Builder 模式都不会么?!!

 昵称10087950 2022-07-26 发布于江苏

疯狂的类构造器

最近栈长在做 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 模式改良的方式,下面来总结一下吧:

  • 常规的 Builder 模式
  • Lombok 实现 Builder 模式(推荐)
  • Java 8 实现 Builder 模式
  • Spring Boot 中的 Builder 模式

如果团队有使用 Lombok,那么 Lombok 无疑是最佳推荐的方式,代码量少,使用简单,方便维护。其他三种实现方式都各有利弊,大家都可以参考使用。

总之,别再写疯狂的类构造器了……

如果你在公司看到同事写这样的优秀代码,请把这篇文章发给他。

好了,今天的分享就到这里了,后面栈长我会更新更多  Java 技术实战及设计模式系列文章,公众号Java技术栈第一时间推送。

本节教程所有实战源码已上传到这个仓库:

https://github.com/javastacks/javastack

最后,觉得我的文章对你用收获的话,动动小手,给个在看、转发,原创不易,栈长需要你的鼓励。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多