设计山寨枚举

作者简介:大家好,我是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对应的descpublic 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

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

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【c语言】重温一下动态内存,int数组过大会造成栈错误

项目场景: 项目场景:互助群同学在刷题的过程中,遇到的一个题目,需要申请一个很大数组,于是这个同学就写了int[1000000],其实这样写也没有错,可是运行后却显示栈错误。于是就找到我来请教,我想就…

Spark-06:共享变量

目录 1.广播变量(broadcast variables) 2.累加器(accumulators) 在分布式计算中,当在集群的多个节点上并行运行函数时,默认情况下,每个任务都会获得函数中使用到的变量的一个副本。如果变量很…

123. 股票买卖的最佳时机III(2次交易)

题目 题解 class Solution:def maxProfit(self, prices: List[int]) -> int:N len(prices)# 状态定义 dp[i][j][k]代表在第i天,被允许完成j次交易时,持有或者不持有的最大利润。k0代表不持有,k1代表持有dp [[[0 for k in range(2)] for…

分布式锁之基于mysql实现分布式锁(四)

不管是jvm锁还是mysql锁,为了保证线程的并发安全,都提供了悲观独占排他锁。所以独占排他也是分布式锁的基本要求。 可以利用唯一键索引不能重复插入的特点实现。设计表如下: CREATE TABLE tb_lock (id bigint(20) NOT NULL AUTO_INCREMENT,…

数据防泄漏系统有什么作用及优势?

数据防泄漏系统(Data Loss Prevention,简称DLP)是一种重要的信息安全解决方案,旨在防止敏感数据被未经授权地泄露、滥用或盗窃。它在保护企业、政府机构和个人的隐私和机密信息方面发挥着关键作用。以下是数据防泄漏系统的作用及优势: 作用&a…

STM32:基本定时器原理和定时程序

一、初识定时器TIM 定时器就是计数器,定时器的作用就是设置一个时间,然后时间到后就会通过中断等方式通知STM32执行某些程序。定时器除了可以实现普通的定时功能,还可以实现捕获脉冲宽度,计算PWM占空比,输出PWM波形&am…

【开源】基于Vue.js的固始鹅块销售系统

项目编号: S 060 ,文末获取源码。 \color{red}{项目编号:S060,文末获取源码。} 项目编号:S060,文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 数据中心模块2.2 鹅块类型模块2.3 固…

YOLOv5 分类模型 预处理 OpenCV实现

YOLOv5 分类模型 预处理 OpenCV实现 flyfish YOLOv5 分类模型 预处理 PIL 实现 YOLOv5 分类模型 OpenCV和PIL两者实现预处理的差异 YOLOv5 分类模型 数据集加载 1 样本处理 YOLOv5 分类模型 数据集加载 2 切片处理 YOLOv5 分类模型 数据集加载 3 自定义类别 YOLOv5 分类模型…

vue3-组件传参及计算属性

​🌈个人主页:前端青山 🔥系列专栏:Vue篇 🔖人终将被年少不可得之物困其一生 依旧青山,本期给大家带来vue篇专栏内容:vue3-组件传参及计算属性 目录 vue3中的组件传参 1、父传子 2、子传父 toRef 与 toRefs vue3中…

杭州银行连接解决方案:集成CRM、用户运营和广告推广系统

自动化与智能化是企业新的增长引擎。在数字化时代,企业需要通过数字化工具来提高效率和效益,这也是杭州银行推出的连接解决方案的初衷。该解决方案集成了CRM、用户运营和广告推广系统,为企业提供全方位的数字化转型支持。 杭州银行连接解决方…

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

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

WPF实战项目十六(客户端):备忘录接口

1、新增IMemoService接口&#xff0c;继承IBaseService接口 public interface IMemoService : IBaseService<MemoDto>{} 2、新增MemoService类&#xff0c;继承BaseService和IMemoService接口 public class MemoService : BaseService<MemoDto>, IMemoService{pub…