Skip to content

本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com

点击↑上方↑蓝色 “编了个程” 关注我~

这是 Yasin 的第 79 篇原创文章

Y 说

有一段时间没更新文章了,看了一下公众号前几篇写的都是 “水文”,最近工作上不算忙,周末闲下来打算写点技术方面的东西。

其实写生活感悟类的文章会轻松一点,因为不需要那么严谨。观众也爱看一点,因为受众面更广。但还是想多写点技术方面的东西,毕竟咱这个号最开始的定位就是编程领域。

最近半年感觉自己对技术上研究得少了,尤其是比较深入的细节,一般是工作上遇到了才会去查资料看一看,感觉比之前少了一股子钻研的劲儿。

我总结下来自己会发生这样的变化是因为 “信念感的降低”。刚毕业那两年执着于能够进到大公司,期望用前沿的技术,面对复杂的挑战,解困难的问题。因为看了网传的各种 “面经”,他们面试确实考察这方面。于是对很多问题也喜欢刨根问底,非要研究个明白。

后来发现实际工作中遇到的绝大多数问题并没有自己想象中的那么复杂,或者说复杂点并不在一些技术细节上面。正是应了那句话:“面试造火箭,工作拧螺丝”。

也许是因为逐渐感觉到写技术文章并不能给自己带来自己期望的收益,ROI 比较低,所以内心本能的动力就没有以前那么强烈了,再加上基因深处的惰性作祟,最近一年都比较散漫,偶尔想写一篇就来一篇,不想写就不写。也有一方面是精力的原因,客观上来讲最近一年比之前忙了不少。

这个问题有点难解,甚至有时候我觉得它不是一个问题,不需要解。当然,“钻研精神”和 “信念感” 都是非常重要的,如果它不是技术,那也应该是一个值得长期追求的东西。简单来说,你有没有一件很明确的事儿,是要长期投入时间和精力去做的。关于这方面的思考,可能后续写一篇 “水文” 再详细聊一下。

以下正文。

什么是枚举

还是有必要先解释一下什么是枚举。枚举对应的英文是 enum,在大多编程语言里也是用这个关键词来定义一个枚举。

->

go 语言默默退出群聊...

<-

我先引用百度百科的解释:在数学和计算机科学理论中,一个集的枚举是列出某些有穷序列集的所有成员的程序,或者是一种特定类型对象的计数。

比较抽象是吧。其实核心就是 “「有穷」” 和 “「所有」” 这两个关键词。比如你可以写一个大于 0 小于 10 的所有正整数的枚举,但不能写一个所有自然数的枚举,因为后者是无穷的。比如性别、星期、月份等等都是常见的枚举。

接下来我想讨论另外两个关于枚举的问题。

枚举是变化的还是不变的?

很显然,有一些枚举值是大概率不变的。比如上面提到的性别、星期、月份等等。

而有些枚举值而可能会随着业务的发展或者世界的发展而变化。比如性别,可能我们刚开始在系统中定义了 “男”、“女”,但后面社会观念开放,也接受一些“中性” 的,或者有些系统有 “保密” 等选项。

更别说有些枚举值本来就是易变的。我在工作中遇到过很多 xx 类型,xx 状态的枚举,在迭代中发生了变更甚至是频繁变更,比如项目类型、任务类型、资金类型等等。

所以枚举有不变的,但也有变化的。「我们如果想要有一个普适性的枚举使用实践的话,最好是把它们都当成可能会发生变化来处理」

枚举值太多怎么办?

有些系统在设计初期,把枚举值很多的也当成了枚举来用,比如国家、省份、组织架构等等。

这种枚举值太多如果写在代码里是非常不利于维护的,而且大多枚举值比较多的枚举,本身就是容易变化的。

所以这类字段虽然也符合枚举的定义,但更适合**「当成正常的业务数据来处理」**,存储在 DB 或者配置中性来使用,而不在代码中一个个定义。在代码中需要判断一些特殊逻辑的地方,用常量去匹配对应的值就行了。

枚举的使用场景

根据我自己的经验,枚举值主要有以下使用场景:

  1. 存储中,如 DB 中

  2. 后端代码中,需要用来做后端逻辑判断

  3. 日志中,打印出入参、关键业务信息的枚举值

  4. 后端返回的异常 message 中

  5. 微服务传参中,作为某个字段

  6. 给前端的传参中,作为某个字段

  7. 前端代码中,用来做前端逻辑判断

  8. 前端显示中,用来下拉列表、表格、详情页等地方显示对应的 label

枚举的使用方式

枚举一般至少会有一个 key,一个 label。比如:male 男,famale 女。

在有些系统中,为了 “节约内存” 这个 key 可能不是一个字符串,而是一个数字,然后 DB 里、接口传参都是用的数字。比如:1 男,2 女。而在某些语言中,甚至可以不要 key,比如 Java,变量名本身就可以作为 key。

使用数字作为 key 有两个问题:

  1. 可读性差,查看数据、排查问题时需要反应一下,甚至是要去翻代码才能知道这个数字代表的是什么意思。

  2. 数字天然有可排序的性质,部分代码可能会不小心利用这个性质,但如果后面想在中间插值,就很困难,需要提前留好 “缝隙”

这里我主要解释一下第二点。我们之前有一个状态的枚举,key 是数字,大概是 1~8 分别表示不同的状态。项目里很多遗留代码里面利用了数字的这个性质来判断逻辑,比如 大于 4 的认为是审批通过,就往下走。

但后面随着业务迭代,我们要在审批通过前插入一个状态 “已废弃”,发现没有位置了,只能改代码,增加了很多风险。

有经验的同事说可以在设计之初就弄成 10、20、30 这样的 key,留点 “缝隙”,后续就可以往中间插入新的枚举值了。可以解决这个问题,但我觉得用字符串作为 key,再给枚举加一个数字的业务字段能更好地解决这个问题。

以现在的数据库、服务器和网络配置,使用数字作为 key 节省那点存储和内存其实没有太大的收益。

最佳实践

DB

MySQL 有 enum 类型,底层是数字,对外显示是字符串。但是需要在字段定义中声明枚举选项。我个人**「不推荐在 DB 中使用专门的 enum 类型,而应该使用字符串类型」**。

在 Spring Data JPA 中,持久化到 DB 默认的方式是枚举在定义中的顺序,从 0 开始。可以通过注解来控制是否使用 name。

@Enumerated(EnumType.ORDINAL)
@Enumerated(EnumType.STRING)


也可以使用@Converter注解来自己写一个 converter 类,这里推荐上面的注解STRING的方式。

代码、日志、异常

代码中如果要使用那肯定是枚举自己的变量引用了。在有些语言中可以直接用 = 或者 switch case 来断言枚举,比较方便。

在打日志的时候,从日志可读性的角度来说,我也是更推荐字符串而不是数字。

在 DDD 架构中,枚举通常具有业务意义,有些团队会放在对应的领域层的包里,有些团队会放在一个比较公共的包里面。这两种方式各有好处,放在领域层的包里,显得更内聚一些。放在公共的包里面,方便跨领域使用枚举,也能让最外层的接口不层直接依赖领域层。具体怎么放团队自己制定一个规范就行。

接口传参

先说微服务的传参。在出入参中直接使用枚举有一些显而易见的好处:可以直接用等号来判断逻辑,能做强校验,不至于传一些 invalid 的值进去。但它也有一些坏处,在阿里的 Java 开发规范中有这么一条关于枚举在接口传参中的使用准则:

这条准则是这么解读的:出参如果使用枚举,后面新增了一个选项,调用方没有升级 jar 包,可能会导致调用方类反序列化出现异常。而入参可以使用,是因为输入是本地决定的,向前兼容不会有问题。

那入参如果使用字符串来代替枚举字段呢?我们可以列一下他们的优劣:

入参使用枚举的好处:

  1. 类型安全,不会传 invalid 的值过去

  2. 代码可读性好,知道这是哪个枚举,有哪些选项,且在接口文档能有所体现

入参使用字符串替代枚举的好处:

  1. 减少了对下游的依赖,比如新增加一个枚举选项,两边同时开发,不用等下游提供新的二方包

  2. 有些枚举可能是更上游传过来的,或者前端传过来的,不用做转换直接透传下去

这么梳理下来,我个人是比较倾向前者的,毕竟类型安全和代码可读性、接口文档都太重要了。

回过头来我们说出参中不能有用枚举,其实是语言和序列化框架的问题。我们在 go 语言 thrift 框架中,出参就用了枚举,用起来还挺香。

前端 label 转换

前端有大量需要把枚举转换层 label 的场景。一般有这样几种解决方案:

  1. 前端自己在代码中也定义一份枚举和 label,和后端保持一致,但需要前后端两边维护

  2. 前端使用后端返回的枚举值和选项去匹配 label,需要后端提供一个专门的枚举集接口

从代码维护性的角度考虑,我个人更倾向于第二种方案,这样统一在一个地方维护。提供一个类似于 get_enums 的接口,大概结构类似这样:

struct GenerateRequest { 
}  
 
struct EnumFieldWithChildren {  
    1: required string          key // 唯一键  
    2: required string          label // 展示标签  
    3: optional string          remark // 注释  
    4: optional list<EnumFieldWithChildren> children // 子枚举
}

struct QueryEnumData {  
    1: required string key // 枚举的key  
    2: required string label // 枚举的展示标签  
    3: optional string remark // 注释  
    7: required list<EnumFieldWithChildren> items // 枚举值列表
}

struct QueryEnumResponse {
    1: required i32 code  
    2: required string msg  
    3: optional list<QueryEnumData> data
}  

QueryEnumResponse QueryEnum(1: GenerateRequest req)


一般这个接口是首次加载的时候请求一下,放在前端的内存里。需要注意的是随着产品的迭代,这个接口可能会变得越来越大,响应体也越来越大,影响加载速度。所以如果枚举值太多,可以按照领域来拆分接口,或者使用参数来控制返回哪些枚举值。

如果是下拉选项的话,有可能会多需要一个 “全部” 或者 “所有” 的枚举选项,这个时候只能前端自己定义了,后端一般不会返回这种枚举选项。前端可以拿到响应后手动插入一个。

前端代码使用

前端除了要显示 label 外,可能代码中也会使用枚举值来判断。这里如果后端定义的就是枚举值,那前端使用起来也会比较方便。

我用 swagger-typescript-api 生成的枚举大概是这样:

export enum PermissionInfoResponseType {  
  API = "API",  
  MENU = "MENU",  
  CUSTOM = "CUSTOM",  
}  


可以看到还是丢了比较多的信息,只保留了枚举的 key,label 没有了,前端如果要用来做 toast 或者有其他显示场景的话,可能还是要去 get_enums 的接口匹配一下 label。

前端也可以自己定义一份,但这样就会有上面提到的,前后端代码都维护的情况。

结论

  1. 枚举尽量不使用数字,除非对存储和内存有非常严苛的需求;

  2. 接口出入参使用枚举类型好处多多,但受限语言和框架的限制,Java 语言出参不建议使用枚举类型;

  3. 通过统一的 get_enums 接口返回枚举信息给前端,前后端尽量只维护一份定义

Released under the MIT License.