当前位置: 首页 >文章 > 同事写了一个疯狂的类构造器,我要疯了,Builder 模式都不会么?!!
收藏
分享

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

举报小虎转载君小虎转载君发布于 2021-08-18763阅读0点赞
拖了半天才看完,到处充满着魔法值不说,把一个类所有参数都放在一个构造器里面...

疯狂的类构造器

最近栈长在做 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.ciphermagic.cn/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



本文原创,未经作者允许不可转载!
更多内容,欢迎关注作者微信公众号:Java技术栈


0条评论
别默默看啦~登录/注册一起参与讨论吧~

暂无评论

请选择举报理由

违反法律法规

侵犯个人权益

有害网站环境

更多训练营>>

为你推荐 · 训练营(全勤打卡报名费全额返累计全额返用户133,673人)

【5月】零基础动态表情包创作训练营
距离开班仅剩13天25人已报名
【6月】人像后期案例实操训练营
距离开班仅剩40天23人已报名
【7月电脑剪映】短视频剪辑入门训练营
距离开班仅剩62天3人已报名
特惠
充值
7折购
今日还在继续学习的你,太棒了!
7
折扣券可用于
年费无限VIP
立 即
使 用
此活动优惠不可与其他活动叠加使用
有效期:000000
消息
登录即可查看消息记录
建议
意见
官方
客服
在线咨询客服热线

您可以与在线客服进行沟通获得帮助

工作日:9:00~22:00节假日:9:00~18:00

联系在线客服

您可以电话联系客服进行沟通获得帮助

工作日:9:30~18:30

400-862-9191
虎课
积分
免费学习89000+个教程!
配套素材、源文件一键下载!
昨日学员已学习了37,480
并提交了326份作业!
登录后立即学习!
loading
微信扫码关注即可登录
您需要同意协议才可以进行登录
登录虎课网,每天免费学课程全站 89000+ 视频会员教程 | 每日可免费学 1
为确保账户信息安全
请先进行真实姓名验证后进行充值付款
立即验证