同事写了一个疯狂的类构造器,我要疯了,Builder 模式都不会么?!!-程序员宅基地

技术标签: delegate  java  dbcp  scrum  javabean  

疯狂的类构造器

最近栈长在做 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技术栈" 原创,原创实属不易,转载、引用本文内容请注明出处,禁止抄袭、洗稿,请自重,尊重他人劳动成果和知识产权。

推荐阅读



点分享点收藏点点赞点在看
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_42232219/article/details/116246490

智能推荐

攻防世界_难度8_happy_puzzle_攻防世界困难模式攻略图文-程序员宅基地

文章浏览阅读645次。这个肯定是末尾的IDAT了,因为IDAT必须要满了才会开始一下个IDAT,这个明显就是末尾的IDAT了。,对应下面的create_head()代码。,对应下面的create_tail()代码。不要考虑爆破,我已经试了一下,太多情况了。题目来源:UNCTF。_攻防世界困难模式攻略图文

达梦数据库的导出(备份)、导入_达梦数据库导入导出-程序员宅基地

文章浏览阅读2.9k次,点赞3次,收藏10次。偶尔会用到,记录、分享。1. 数据库导出1.1 切换到dmdba用户su - dmdba1.2 进入达梦数据库安装路径的bin目录,执行导库操作  导出语句:./dexp cwy_init/[email protected]:5236 file=cwy_init.dmp log=cwy_init_exp.log 注释:   cwy_init/init_123..._达梦数据库导入导出

js引入kindeditor富文本编辑器的使用_kindeditor.js-程序员宅基地

文章浏览阅读1.9k次。1. 在官网上下载KindEditor文件,可以删掉不需要要到的jsp,asp,asp.net和php文件夹。接着把文件夹放到项目文件目录下。2. 修改html文件,在页面引入js文件:<script type="text/javascript" src="./kindeditor/kindeditor-all.js"></script><script type="text/javascript" src="./kindeditor/lang/zh-CN.js"_kindeditor.js

STM32学习过程记录11——基于STM32G431CBU6硬件SPI+DMA的高效WS2812B控制方法-程序员宅基地

文章浏览阅读2.3k次,点赞6次,收藏14次。SPI的详情简介不必赘述。假设我们通过SPI发送0xAA,我们的数据线就会变为10101010,通过修改不同的内容,即可修改SPI中0和1的持续时间。比如0xF0即为前半周期为高电平,后半周期为低电平的状态。在SPI的通信模式中,CPHA配置会影响该实验,下图展示了不同采样位置的SPI时序图[1]。CPOL = 0,CPHA = 1:CLK空闲状态 = 低电平,数据在下降沿采样,并在上升沿移出CPOL = 0,CPHA = 0:CLK空闲状态 = 低电平,数据在上升沿采样,并在下降沿移出。_stm32g431cbu6

计算机网络-数据链路层_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输-程序员宅基地

文章浏览阅读1.2k次,点赞2次,收藏8次。数据链路层习题自测问题1.数据链路(即逻辑链路)与链路(即物理链路)有何区别?“电路接通了”与”数据链路接通了”的区别何在?2.数据链路层中的链路控制包括哪些功能?试讨论数据链路层做成可靠的链路层有哪些优点和缺点。3.网络适配器的作用是什么?网络适配器工作在哪一层?4.数据链路层的三个基本问题(帧定界、透明传输和差错检测)为什么都必须加以解决?5.如果在数据链路层不进行帧定界,会发生什么问题?6.PPP协议的主要特点是什么?为什么PPP不使用帧的编号?PPP适用于什么情况?为什么PPP协议不_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输

软件测试工程师移民加拿大_无证移民,未受过软件工程师的教育(第1部分)-程序员宅基地

文章浏览阅读587次。软件测试工程师移民加拿大 无证移民,未受过软件工程师的教育(第1部分) (Undocumented Immigrant With No Education to Software Engineer(Part 1))Before I start, I want you to please bear with me on the way I write, I have very little gen...

随便推点

Thinkpad X250 secure boot failed 启动失败问题解决_安装完系统提示secureboot failure-程序员宅基地

文章浏览阅读304次。Thinkpad X250笔记本电脑,装的是FreeBSD,进入BIOS修改虚拟化配置(其后可能是误设置了安全开机),保存退出后系统无法启动,显示:secure boot failed ,把自己惊出一身冷汗,因为这台笔记本刚好还没开始做备份.....根据错误提示,到bios里面去找相关配置,在Security里面找到了Secure Boot选项,发现果然被设置为Enabled,将其修改为Disabled ,再开机,终于正常启动了。_安装完系统提示secureboot failure

C++如何做字符串分割(5种方法)_c++ 字符串分割-程序员宅基地

文章浏览阅读10w+次,点赞93次,收藏352次。1、用strtok函数进行字符串分割原型: char *strtok(char *str, const char *delim);功能:分解字符串为一组字符串。参数说明:str为要分解的字符串,delim为分隔符字符串。返回值:从str开头开始的一个个被分割的串。当没有被分割的串时则返回NULL。其它:strtok函数线程不安全,可以使用strtok_r替代。示例://借助strtok实现split#include <string.h>#include <stdio.h&_c++ 字符串分割

2013第四届蓝桥杯 C/C++本科A组 真题答案解析_2013年第四届c a组蓝桥杯省赛真题解答-程序员宅基地

文章浏览阅读2.3k次。1 .高斯日记 大数学家高斯有个好习惯:无论如何都要记日记。他的日记有个与众不同的地方,他从不注明年月日,而是用一个整数代替,比如:4210后来人们知道,那个整数就是日期,它表示那一天是高斯出生后的第几天。这或许也是个好习惯,它时时刻刻提醒着主人:日子又过去一天,还有多少时光可以用于浪费呢?高斯出生于:1777年4月30日。在高斯发现的一个重要定理的日记_2013年第四届c a组蓝桥杯省赛真题解答

基于供需算法优化的核极限学习机(KELM)分类算法-程序员宅基地

文章浏览阅读851次,点赞17次,收藏22次。摘要:本文利用供需算法对核极限学习机(KELM)进行优化,并用于分类。

metasploitable2渗透测试_metasploitable2怎么进入-程序员宅基地

文章浏览阅读1.1k次。一、系统弱密码登录1、在kali上执行命令行telnet 192.168.26.1292、Login和password都输入msfadmin3、登录成功,进入系统4、测试如下:二、MySQL弱密码登录:1、在kali上执行mysql –h 192.168.26.129 –u root2、登录成功,进入MySQL系统3、测试效果:三、PostgreSQL弱密码登录1、在Kali上执行psql -h 192.168.26.129 –U post..._metasploitable2怎么进入

Python学习之路:从入门到精通的指南_python人工智能开发从入门到精通pdf-程序员宅基地

文章浏览阅读257次。本文将为初学者提供Python学习的详细指南,从Python的历史、基础语法和数据类型到面向对象编程、模块和库的使用。通过本文,您将能够掌握Python编程的核心概念,为今后的编程学习和实践打下坚实基础。_python人工智能开发从入门到精通pdf

推荐文章

热门文章

相关标签