设计山寨枚举

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

一个需求

在Employee类中,定义一个字段,用来表示在哪一天休息(星期几)。

最简单的设计是这样的:

@Data
public class Employee {
    /**
     * 指定员工在哪一天休息
     */
    private Integer restDay;
}

使用时,只要传入1~7即可为employee对象指定具体的休息日:

public class EmployeeDemo {
    public static void main(String[] args) {
        Employee employee = new Employee();
        employee.setRestDay(1);
    }
}

@Data
class Employee {
    /**
     * 指定员工在哪一天休息
     */
    private Integer restDay;
}

用常量类改进

但是上面的代码有两个问题:

  • 业务含义不明确,在一部分人的认知里,1代表周日,而不是周一,可能会传错
  • 代码没有做任何限制,调用者可以传入任意数字,甚至是负数,这是不合逻辑的

有人可能想到了用接口常量或类常量来解决以上问题,比如:

public class EmployeeDemo {
    public static void main(String[] args) {
        Employee employee = new Employee();
        employee.setRestDay(WeekDay.MONDAY);
    }
}

@Data
class Employee {
    /**
     * 指定员工在哪一天休息
     */
    private Integer restDay;
}

/**
 * 常量类
 */
class WeekDay {
    public static final Integer MONDAY = 1;
    public static final Integer TUESDAY = 2;
    public static final Integer WEDNESDAY = 3;
    public static final Integer THURSDAY = 4;
    public static final Integer FRIDAY = 5;
    public static final Integer SATURDAY = 6;
    public static final Integer SUNDAY = 7;
}

自定义枚举改进

但实际上,常量类仅仅是解决了第一个问题:业务含义明确,代码可读性提高。但调用者仍然可以随意传参,比如仍然允许传入-1。

如果希望对入参进行限制,可以对POJO的set方法进行约束:

然而抛异常并不是最优解,虽然确实最终阻止了错误发生,但是太迟了!调用者在编写代码时仍然可能在毫不知情的情况下写出setRestDay(-1)这样的语句(IDEA只会提示传入Integer类型,却不会提示范围是1~7)。

《Effective Java》的作者说过:编译期错误优于运行期错误,如果一段代码注定会出错,应该尽早暴露以便在编译期就解决问题。但是Java编译器只会做语法检查,不会做逻辑运算。

怎么办?

要对方法的形参进行限制,无非从两个方面考虑:

  • 变量类型(已约束)
  • 变量范围(未约束)

变量类型已经被定为Integer,很大程度上阻止了String、Double等其他类型的参数传入,但变量的范围还没有得到约束。但是你想过为什么用户能传入-1吗?因为Integer本身的范围就是-2147483648 至 2147483647,包含了-1。

如果存在一种Xxx类型,它只有7个元素,分别代表周一到周日,那么我们把它作为setRestDay(Xxx xxx)的类型,不仅约束了变量类型(只能是Xxx类型),还约束了变量范围(只有7个)!

很明显,Java的8大基本类型都不符合。

基本类型

字节数

位数

最大值

最小值

byte

1byte

8bit

2^7 - 1

-2^7

short

2byte

16bit

2^15 - 1

-2^15

int

4byte

32bit

2^31 - 1

-2^31

long

8byte

64bit

2^63 - 1

-2^63

float

4byte

32bit

3.4028235E38

1.4E - 45

double

8byte

64bit

1.7976931348623157E308

4.9E - 324

char

2byte

16bit

2^16 - 1

0

最重要的不是范围太大,而是基本类型的范围不能按我们的需要改变。即,可选范围不能根据业务定制。

那我们只剩一条路:根据业务自定义类型。

基本类型无法自定义,所以我们只能新建引用类型。再具体点,就是新建一个类

怎样才能限制Xxx类只有7个元素呢?

不要走回头路:

Xxx.MONDAY和WeekDay.MONDAY本质上没啥区别,就是换了个类名而已。

但是IDEA的错误提示却给了我们灵感:

也就是说,此时restDay需要的是Xxx类型的变量,而不是Xxx.MONDAY。解决问题的一个思路是想办法把Xxx.MONDAY变成Xxx类型。

这听起来很诡异,Xxx.MONDAY竟然是Xxx类型?!

先别想这么多,按这个思路写一下。是不是这样:

class Xxx {
    public Xxx MONDAY;
}

也即是说,字段类型是Xxx。

所以原先的代码可以改成这样:

OK,employee.setRestDay(Xxx.MONDAY)总算通过了。

我们再把类名改一下,换个有意义的名字:

初步完成,但别急,停下来仔细看看图中的代码,尝试理解。

理解了吧?

现在我告诉你,上面的代码还是有问题。

类型确实限制为WeekDay,但并没有限制范围。我们完全可以不从WeekDay拿,自己在外面new一个即可:

如何限制外部随意创建某个类的呢?对,单例模式:

外界只能从WeekDay取出设定好的7个对象,这下restDay字段的类型和范围都限制住了。

为枚举添加字段,让含义更明确

通过单例模式,我们新建了WeekDay,既解决了业务含义不明确的问题(MONDAY见名知意),又对入参做了限制(只能从WeekDay获取设定的7个元素)。但我总觉得原先的MONDAY=1更顺眼,MONDAY=new WeekDay()看起来怪怪的。

是的,直面你内心的疑惑:restDay字段的值是WeekDay对象,存入数据库后会变成什么?

我们原本打算用1~7代表一周七天,只不过为了可读性限定范围,才搞了单例模式,但心里还是希望数据库存的是1~7。

所以我们必须让MONDAY、TUESDAY这些对象具备特征,最终和1~7形成对应关系。

由于在我们的项目中,1就代表周一,不希望被更改,所以我们可以给WeekDay加上final修饰的属性:

为什么提示我属性可能没有初始化呢?这就要看大家final掌握得如何了。final关键字的赋值有以下几种方式:

  • 显式赋值:private final Integer code = 1
  • 静态代码块/代码块赋值
  • 构造器赋值

因为final变量只能赋值一次(不可变),如果不赋值就是默认值,是没有意义的。就好比你想要一个水桶,希望可以存水,但是它的默认值是水泥,而且出厂以后就不能改了...结果你拿到一个装满水泥的水桶,毫无意义。

所以JDK会强制你给final字段赋值,以保证final字段存的是你期望的值。而创建对象时一定会经历显式赋值、代码块赋值、构造器赋值三个时期,只要在任意一个时期为final字段赋值即可保证对象创建后必然有初始值。

然而构造器有点特殊,因为一个对象可以同时拥有多个构造器。即使准备了Constructor A为final字段初始化,调用者仍可以使用无参构造或者Constructor B创建(假设B不给final字段赋值),如此一来final还是没有被赋值。

所以,当前案例使用final字段时必须禁用无参构造,强制走有参构造,确保final字段初始化。

代码修改如下:

  • 去除空参构造 、设置唯一的有参构造为private,禁止外界new对象并强制为final字段赋值
  • 提供getter方法(不需要setter,因为反正字段是final,无法改变)

枚举与数据库

一部分人可能从来没试过用MyBatis向数据库插入带有复杂类型的POJO:

你们可能认为,最坏的结果是序列化存入JSON:

但实际上即使我把数据库Column设置为JSON类型也无法插入restDay:

因为如果不作任何配置,MyBatis默认只能处理简单类型和常见的引用类型,比如String、Integer等,对于复杂类型(自定义类、枚举)会自动忽略:

那么,如何处理POJO中复杂类型的字段呢?

通常来说我们会写一个转换器,不论存入还是取出,都要经过转换器:

  • 存入:从restDay中取出code存入数据库
  • 取出:根据code找到对应的WeekDay赋值给restDay

数据库实际存储的一般不会是整个WeekDay对象,而是WeekDay.code或者WeekDay.desc。

具体如何转换复杂对象,我们会在后续章节介绍。

但我个人有时懒得写转换器,都是直接用简单类型:

这个时候也就不存在什么转换了,你可以理解为就是以前的方式,就是Integer restDay。此时类型已经限制成Integer,但范围需要我们自己控制。可以在WeekDay中新增一个of()方法,用来校验前端传来的code是否合法:

@Getter
class WeekDay {
    public static final WeekDay MONDAY;
    public static final WeekDay TUESDAY;
    public static final WeekDay WEDNESDAY;
    public static final WeekDay THURSDAY;
    public static final WeekDay FRIDAY;
    public static final WeekDay SATURDAY;
    public static final WeekDay SUNDAY;

    private static final WeekDay[] VALUES;

    static {
        // 之前说过,final字段赋值有三种形式,现在我们换成静态代码块赋值
        MONDAY = new WeekDay(1, "星期一");
        TUESDAY = new WeekDay(2, "星期二");
        WEDNESDAY = new WeekDay(3, "星期三");
        THURSDAY = new WeekDay(4, "星期四");
        FRIDAY = new WeekDay(5, "星期五");
        SATURDAY = new WeekDay(6, "星期六");
        SUNDAY = new WeekDay(7, "星期日");
        // 在加载类时就收集所有的WeekDay对象
        VALUES = new WeekDay[]{
                MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
        };
    }

    /**
     * 校验前端传入的code是否合法
     *
     * @param code
     * @return
     */
    public static WeekDay of(Integer code) {
        for (WeekDay weekDay : VALUES) {
            if (weekDay.code.equals(code)) {
                return weekDay;
            }
        }
        // 如果根据code找不到对应的WeekDay,说明code范围不对,是非法的
        throw new IllegalArgumentException("Invalid Enum code:" + code);
    }

    private WeekDay(Integer code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    private final Integer code;
    private final String desc;
}
// 伪代码
public void saveUser(User user){
    // 校验一下
    WeekDay.of(user.getRestDay);
    // 插入
    userMapper.insertSelective(user);
}

请大家特别注意上面的新写法:用static静态代码块初始化final字段 + 用VALUE数组收集所有枚举单例对象。

这样我们就控制住了数值范围。当然,这个并不是编译期错误,本质上还是和一开始的处理方式一样:

讲完数据存入,接下来聊聊取出数据后怎么处理。

枚举与前端

比如我们从数据库查出一个Employee对象:

{
	"name": "bravo",
	"department": "技术部",
	"restDay": 1
}

难道前端这样写?

if (employee.restDay == 1) {
    $("#restDay").val("星期一");
} else if (employee.restDay == 2) {
    $("#restDay").val("星期二");
} else if (employee.restDay == 3) {
    $("#restDay").val("星期三");
} else if (employee.restDay == 4) {
    $("#restDay").val("星期四");
} else if (employee.restDay == 5) {
    $("#restDay").val("星期五");
} else if (employee.restDay == 6) {
    $("#restDay").val("星期六");
} else if (employee.restDay == 7) {
    $("#restDay").val("星期日");
}

这种转换工作最好在后端完成,理由是:后端更清楚各个状态的对应关系。所以我们应该在接口返回结果之前,就把转换工作完成,最终传递"星期一"而不是1。为此我们需要做两步:

  • Employee新增private String restDayDesc字段
  • 新增 getDescByCode()方法

最终代码:

@Getter
class WeekDay {
    public static final WeekDay MONDAY;
    public static final WeekDay TUESDAY;
    public static final WeekDay WEDNESDAY;
    public static final WeekDay THURSDAY;
    public static final WeekDay FRIDAY;
    public static final WeekDay SATURDAY;
    public static final WeekDay SUNDAY;

    private WeekDay(Integer code, String desc) {
        this.code = code;
        this.desc = desc;
    }
    
    private static final WeekDay[] VALUES;
    
    static {
        MONDAY = new WeekDay(1, "星期一");
        TUESDAY = new WeekDay(2, "星期二");
        WEDNESDAY = new WeekDay(3, "星期三");
        THURSDAY = new WeekDay(4, "星期四");
        FRIDAY = new WeekDay(5, "星期五");
        SATURDAY = new WeekDay(6, "星期六");
        SUNDAY = new WeekDay(7, "星期日");
        VALUES = new WeekDay[]{
                MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
        };
    }

    private final Integer code;
    private final String desc;

    // 返回所有的对象
    public static WeekDay[] values() {
        return VALUES;
    }

    // 遍历对象,根据code返回code对应的desc
    public static String getDescByCode(Integer code) {
        WeekDay[] weekDays = WeekDay.values();
        for (WeekDay weekDay : weekDays) {
            if (weekDay.getCode().equals(code)) {
                return weekDay.getDesc();
            }
        }
        throw new IllegalArgumentException("Invalid Enum code:" + code);
    }
}
public User getUser(){
    User user = userMapper.selectByPrimaryKey(1L);
    // 为user设置restDayDesc,方便前端展示
    user.setRestDayDesc(WeekDay.getDescByCode(user.getCode()));
    return user;
}

打印结果

Employee{restDay=1, restDayDesc='星期一'}

后话

正如上面介绍的,你可以在DO的字段上直接使用枚举类型,但是要编写相对应的转换器:

关于MyBatis如何转换枚举,请参考后面的章节。

在本文中,我们退而求其次,演示了把restDay字段设置为Integer,然后人工转换的办法:

《阿里巴巴开发手册》中关于枚举有以下描述:

总之,不推荐返回值对象中直接使用枚举。

但大家肯定见过Result类中的这种写法:

但它并没有把枚举对象返回,ExceptionCodeEnum作为入参传入后,其实就被分解为code和desc了,是很普通的Integer和String类型。

以上是对山寨版"枚举"的讨论,下篇我们将讲解正版枚举的用法,但篇幅会短很多,因为和山寨"枚举"太像了,只要再介绍一下正版枚举的其他特性即可。

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/183606.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

2000-2022年上市公司全要素生产率LP方法(含原始数据+测算代码do文档+计算结果)

2000-2022年上市公司全要素生产率测算LP法(含原始数据测算代码do文档计算结果) 1、时间:2000-2022年 2、范围:上市公司 3、指标:证券代码、证券简称、统计截止日期、固定资产净额、year、股票简称、报表类型编码、折…

shell 条件语句 if case

目录 测试 test测试文件的表达式 是否成立 格式 选项 比较整数数值 格式 选项 字符串比较 常用的测试操作符 格式 逻辑测试 格式 且 (全真才为真) 或 (一真即为真) 常见条件 双中括号 [[ expression ]] 用法 &…

【Git】git 更换远程仓库地址三种方法总结分享

因为公司更改了 gitlab 的网段地址,发现全部项目都需要重新更改远程仓库的地址了,所以做了个记录,说不定以后还会用到呢。 一、不删除远程仓库修改(最方便) # 查看远端地址 git remote -v # 查看远端仓库名 git rem…

【Web题】狼追兔问题

💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

Visual Studio 2019 C# System.BadImageFormatException 解决方法

文章目录 1.DLL文件缺失或不匹配原因解决方法 2.系统环境变量Path下内容过多原因解决方法 3.位数错误原因解决方法 分析几种可能因素 1.DLL文件缺失或不匹配 原因 检查对应Debug路径下的DLL文件是否有缺失 解决方法 将对应的DLL文件放到Debug文件夹里面,检查冗余…

浅谈JDK动态代理(下)

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO 联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬 动态代理的使命有两个&…

【开源】基于Vue.js的用户画像活动推荐系统

项目编号: S 061 ,文末获取源码。 \color{red}{项目编号:S061,文末获取源码。} 项目编号:S061,文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 数据中心模块2.2 兴趣标签模块2.3 活…

visionOS空间计算实战开发教程Day 6 拖拽和点击

在之前的学习中我们在空间中添加了3D模型,但在初始摆放后就无法再对其进行移动或做出修改。本节我们在​​Day 5​​显示和隐藏的基础上让我们模型可以实现拖拽效果,同时对纯色的立方体实现点击随机换色的功能。 首先是入口文件,无需做出改变…

leedcode 刷题 - 除自身以外数组的乘积 - 和为 K 的子数组

I238. 除自身以外数组的乘积 - 力扣(LeetCode) 给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在…

『Linux升级路』基础开发工具——gcc/g++篇

🔥博客主页:小王又困了 📚系列专栏:Linux 🌟人之为学,不日近则日退 ❤️感谢大家点赞👍收藏⭐评论✍️ 目录 一、快速认识gcc/g 二、预处理 📒1.1头文件展开 📒1…

qgis添加postgis数据

左侧浏览器-PostGIS-右键-新建连接 展开-双击即可呈现 可以点击编辑按钮对矢量数据编辑后是直接入库的,因此谨慎使用。

Rust语言入门教程(一) - 简介及Cargo使用

Rust编程入门 为什么学习Rust 我本人是一个DevOps工程师,并不是专职的开发人员,但需要了解各种各样的语言的基本知识和特性,以便在不同的项目中帮助开发人员设计软件架构,部署流程以及进行错误排查和调试。但是对任何新生的优秀…

Linux常用命令——blkid命令

在线Linux命令查询工具 blkid 查看块设备的文件系统类型、LABEL、UUID等信息 补充说明 在Linux下可以使用blkid命令对查询设备上所采用文件系统类型进行查询。blkid主要用来对系统的块设备(包括交换分区)所使用的文件系统类型、LABEL、UUID等信息进行…

JavaScript异步编程

同步与异步 #先看 2 段代码 <span style"background-color:#282c34"><span style"color:#2c3e50"><span style"color:#cccccc"><code><span style"color:#999999">//代码1</span> <span styl…

C++学习之路(一)什么是C++?如何循序渐进的学习C++?【纯干货】

C是一种高级编程语言&#xff0c;是对C语言的扩展和增强。它在C语言的基础上添加了面向对象编程&#xff08;OOP&#xff09;的特性&#xff0c;使得开发者能够更加灵活和高效地编写代码。 C的名字中的“”符号表示在C语言的基础上向前发展一步&#xff0c;即“加加”&#x…

多功能回馈式交流电子负载的应用

多功能回馈式交流电子负载是用于模拟和测试电源、电池等电子设备的负载工具。它具有多种应用&#xff0c;可以用于测试和评估各种类型的电源&#xff0c;包括直流电源和交流电源。它可以模拟各种负载条件&#xff0c;如恒定电流、恒定电压和恒定功率&#xff0c;以验证电源的性…

电脑技巧:推荐八个非常实用的在线网站值得收藏

目录 1、wikihow 干货分享网站 2、次元小镇 二次元必备网站 3、AI创作家 4、SKRbt 搜索引擎网站 5、barbg 全球资源网站 6、书签地球 7、4KHDR世界 8、a real me 今天小编给大家推荐八个非常实用的在线网站值得收藏&#xff01; 1、wikihow 干货分享网站 这个网站是一…

微信小程序制作

如果你也想搭建一个小程序&#xff0c;但不知道如何入手&#xff0c;那么今天我就教你如何使用第三方制作平台&#xff0c;在短短三十分钟内搭建一个小程序。 一、登录小程序制作平台 首先&#xff0c;登录到小程序制作平台的官方网站或应用程序&#xff0c;进入后台管理系统。…

geemap学习笔记013:为遥感动态GIF图添加图名

前言 遥感动态GIF图可以展示地理区域随时间的变化&#xff0c;这对于监测自然灾害、湿地变化、城市扩展、农田变化等方面非常有用&#xff0c;并且可以反复观察图像&#xff0c;以更深入地了解地表的动态变化。本节主要是对遥感动态GIF图添加图名&#xff0c;以便于更好地理解…

【Redis】前言--redis产生的背景以及过程

一.介绍 为什么会出现Redis这个中间件&#xff0c;从原始的磁盘存储到Redis中间又发生了哪些事&#xff0c;下面进入正题 二.发展史 2.1 磁盘存储 最早的时候都是以磁盘进行数据存储&#xff0c;每个磁盘都有一个磁道。每个磁道有很多扇区&#xff0c;一个扇区接近512Byte。…