工程实践中关于命名那些事

2021/10/05

前言

在我还在读书还没开始写工程代码的时候,我就一直看到很多行业的前辈在玩梗,大概意思就是说写代码五分钟,取名字两小时。那时没实际的项目经验,也就是觉得一乐,等自己真切的在工程实践过程中,真的是能感受到共鸣。

国外有过一个很有意思的投票统计,让程序员选出一个平时工作中认为最难的事情,大家可能会觉得方案设计或者看改别人代码应该是最难,结果几乎一半人都投给了命名相关的事情。

image-20211004130401300

在工程实践这种命名是在普通不过的一件事,但是想要把做这件事做好做优并非易事。大家回想一下,平时大家都是怎么命名的呢,有固定的命名习惯吗,还是临场发挥,查一查单词感觉差不多就开始写逻辑?今天某个方法叫fetchXXX,明天某个方法叫findXXX,甚至叫XXXFind。因为你每次都随便想,导致你命名风格乱七八糟,你大概率会记不起你的写过的方法名,要吭哧吭哧找半天。

我是非常建议花个半小时总结一下然后形成自己命名风格,就不用每次命名都要纠结几分钟,还怕写low了同事笑话。写代码的速度upup提升,并且质量也upup提升。我自己也做了一些总结,自己平时用的比较习惯的一些命名习惯在这分享一下,大家可以自己花一点时间参考一下我这个,然后制定出符合自己喜好和团队习惯的风格。

规约

java这边常见的规约我就不多说了,阿里的P3C已经提供了比较专业的思路或者参照GJSG

  • 包名采用全部小写
  • 类名使用大驼峰(帕斯卡法)
  • 方法名和普通变量名都采用小驼峰
  • 静态成员变量、枚举值、常量使用下划线分割全部大写(除常量外,Java命名习惯中均不推荐使用下划线等字符)

不管是用什么风格,这都是为了区分功能和好看而已,命名最重要的就是见名知意,最高境界就是代码即注释

url路径命名

url路径命名的重要性,我觉得在于后期的维护以及直观性。因为名字没取好的话随着迭代后面的兼容以及其他项目多了很难一眼知道这个路径是干什么,尤其是rpc调用的时候。

对于这个资源定位符命名我觉得很奇怪,各个大厂出奇的不一致,也没有很好地命名规范。就我自己而言会在团队里面倡导restful的命名规范,虽然大家写的可能都是restfulApi,但是一些规则可能确实没有强遵守,平时也没有注意这块,我介绍一下我自己比较推崇的格式。

格式: http(s)://host/{app-name}/{version}/{domain}/{rest-convention}

{app-name}: 标记name在这里很方便来解决跨域问题,例如原来前后端使用子域名的方式来通信,就很容易出现跨域问题,不过我看大部分厂的C端产品接口都固定写’api’

{version}:代表api的版本信息,这是restful推崇的,一般v1、v2、v3等来做区别 {domain}:用来定义任何技术的区域,微服务下我们一般这个就是模块的业务名字 {rest-convention}:是这个域(domain)下,约定的rest接口集合

除了在遵循格式以外,还有一些restful推荐的规范

  • URL路径中应当全部使用小写字母
  • URL连词时应当使用连字符隔开( - ),而不是使用下划线( _ )
  • URL只定位资源,不应当出现动词(动作应该在method中提现)

下面放一个饿了么看自己吃货豆的一个接口,路径命名写起来很规范,看一眼能定位到业务域也可以很快明白接口的作用,不过路径上用了小驼峰,个人感觉路径长了就稍稍没那么直观。这都是个人喜好了,我看了很多阿里系的接口,它们非常喜欢在URL上面用大写字母。

GET /restapi/v1/users/supervip/pea/queryAccountBalance?types={}&longitude={}&latitude={}

一般类命名

一般类就是大家写的比较多的业务类和功能类,这个类命名重要性我觉得更多的实际意义在于方便找和后期维护。项目大了,修改很早以前的需求需要翻好久,如果有良好的命名习惯,根据自己的习惯一下就可以找到了。还有类名写的稀烂的话,后期加需求考虑单一职责问题都不知道加到哪里,要么随便找个地方塞一下,要么新开一个。时间长了根本记不住,找又找半天。最可怕的时候对于要接收你工作的同事,简直是灾难。下面就是一些我总结的习惯,分享一下。

  1. 枚举类使用”Enum”结尾 e.g. GroupAppTypeEnum
  2. 常量统一管理接口使用”Constants”作为结尾(会定义成接口) e.g. TopicConstants
  3. 抽象类使用“Abstract”做为开头 e.g. AbstractDistributedLock
  4. 自定义异常类命名使用“Exception”做为结尾 e.g. HeartbeatException
  5. Test类使用“Test”做为结尾 e.g. StudyRoomInitTest
  6. 控制器使用“Controller”结尾 e.g. LearningGroupSubChannelController
  7. 业务处理接口使用“Service”结尾(不建议使用I开头),实现类使用“ServiceImpl”结尾 e.g. LearningFeedStoryStatusServiceImpl
  8. 持久层的封装的话…结尾使用“Dao”、”Mapper”、”Repository”都可以,主要还是看项目一以贯之的风格和团队习惯,如果是我主导的新项目我会把mongdb、redis、influxdb等nosql或者多存储介质联合获取命名成Repository结尾,mysql、postgrep之类的RDBMS命名为Dao结尾 e.g. AggHealthMongoRepository

我也随便写一个写了一个格式,一般我命名就直接套用在这个

{domain}{subLabel}{businessType}{classType}

{domain}:你的业务域

{subLabel}:子分类

{business}:业务操作对象

{classType}:作用,service还是controller什么的

比如,我自己现在写的项目是这么一个场景,我有一个很多海量数据要计算,因为要实时然后我再中间加了一层单独做了服务专门做聚合数据,聚合数据有几种分类,每个分类下面又有不同而类型。我写一个业务实现类我可能就会命名成

/**
agg表名聚合业务,health表示聚合业务的健康类型
HeartRate表示健康下心率业务,ServiceImpl表示业务逻辑的实现类
**/
AggHealthHeartRateServiceImpl

然后有时候业务之间没有明显的分级,那就直接写一个业务域就好了,如果项目小一些常量类或者枚举类可以就每个模块写一个就好了,这个业务域范围可以根据类作用不同提升和降低。命名应当和业务密切相关,做到单一职责。

实体类命名

实体类这里就设计很多反小驼峰的命名,阿里要求都大写,但是这一点我不想遵循阿里规范,我觉得XxxVo比XxxVO好看,从常理来说你XML可以写成Xml为啥VO不能写成Vo。很幸运我同事和我想法一样,我们在自己代码里面用的都是很开心的使用小驼峰。

image-20211005015318338

阿里规约

下面是我个人习惯

  • 请求body参数对象使用Request结尾 e.g. UserLearningArchiveRequset
  • 返回展示对象使用Vo结尾 e.g. LearningGroupNoteVo
  • 传输层对象使用Dto结尾 e.g. LearningGroupUpgradeDto
  • es实体类使用Do结尾 e.g. ChatGroupInfoDo
  • mongo实体类使用Doc结尾 e.g. LearningGroupInfoDoc
  • db实体类命名和转大驼峰和表名(主要方便使用mybatis) e.g. TUser

这个整体命名跟着业务走就好了,如果比较复杂容易混可以在前面加一个域给定范围,但是我觉得尽量都应该给一个域,名字长一点是没关系,就怕说不清楚和给后面带来麻烦。例如一个群升级的dto,刚开始你就写成UpgradeDto;后面team升级和group升级。改成GroupUpgradeDto;后面又有learningGroup和chatGroup升级,然后又改成LearningGroupUpgradeDto。

最骚的是往往真实情况是这样的,你刚开始群升级写成UpgradeDto,后面多了小分队升级,你又想用UpgradeDto结果发现已经存在了,先思考半天这是啥东西,后面记起来是群升级的,然后为了不冲突新建了一个teamGroupUpgradeDto,然后就不管了。后面代码维护的时候发现有一个UpgradeDto和teamGroupUpgradeDto,他们其实是并列的关系,但是从名字上看不出,这会让后面维护的人很困惑甚至不敢动这代码。

变量命名

在一个工程里面,如果变量的命名很糟糕对后续维护的人员来说简直是噩梦,更可怕的是..自己也容易忘记,其他的类或者方法还是注释之类的辅助理解,一般变量很少写注释,我想没有那个人愿意阅读这样的代码。原来我和一个前同事讨论过代码到底是给人看还是给机器看,我的观点一直很鲜明,代码肯定是给人看的,人家机器压根就读不懂你这个编译前的代码,机器编译解释后在跑的代码你也看不懂。

我是非常不喜欢一些c类语言的ijk命名,甚至在一些demo和刷题我拒绝使用单字母命名,尽量想让命名有意义一些。我看过很多写golang的前辈,依然保持着这样的单字母风格….能省则省,我不理解….

变量名的固定套路是使用名词(词组)或者形容词,一般名词用于普通变量,形容词用于布尔类型

我这有几个要领,可以让你写出好的变量名

  • 变量名字前后需要统一

再同一个工程里面,相同含义的变量名尽量前后统一。比如表示总和,其实sum和total都可是表示,但是尽量一旦使用了sum,相同场景下就不用再使用total了

  • 复数风格需要统一

如果有一个LIst,你会怎么命名呢,其实students和studentList都可以。我更喜欢用后者,这样到后面使用的时候看名字就可以知道集合容器是什么,可以表示studentSet等等,不需要小鼠标放上面。当然这都是个人喜好,最关键的是尽量风格统一,students和studentList不要同时出现,不然给别人和以后的自己带来困惑

  • 布尔类型不要is开头

这个是阿里规范手册中提到过的,虽然我项目中有过这样的命名,但是写代码还是遵守公认的规范,指不定哪一天就踩坑了。那么布尔一般怎么命名呢,我自己习惯把is换成bool,就是isAlive写成boolAlive。

image-20211005212332972

阿里规范手册中说明
  • 不要怕长,避免使用缩写

这个是我刚开始工程实际的时候很纠结的点,写全称又非常长,写简写又觉得不直白。现在我的处理是只有约定俗成的缩写才缩,否则一律写全。比如你写一个cnt,我脑袋里面content、context、count、contrast再脑袋里面飘过,二义性太严重了。下面附一张常见缩写表

全称 缩写
identification id
average avg
maximum max
minimum min
error err
message msg
image img
length len
library lib
password pwd

方法命名

写java业务逻辑大部分都是在调用方法,不管是你的方法还是别人写的方法,随便debug下看一眼方法栈都有上百个方法,所以方法名是很重要。名字取的直白可以让阅读代码的人不需要看完全部代码,只需要浏览主方法的主干就可以很快get到这个方法在干什么。

方法名说简单很简单,说难也很难。简单是它有固定套路,一般就是动词加名词的组合,说难是因为动词要精准,名字要专业。

{动词}{名字} fetchUserList()

我这里有几个要领,可以让你写出好方法名

  • 动词要精准

例如一个叫addCharacter(),一咋看,还不错,从方法字面意思看是给某个字符串添加一个字符。但是到底是在原有字符串首部添加,还是在原有字符串末尾追加呢?从方法名字完全看不出来这个方法的真正意图,只能继续往下读这个方法的具体实现才知道。但是如果我叫appendCharacter()呢,是不是精准那味马上就上来了。

动词选得好,方法名更加容易让人看懂,下面附一张动词表

类别 单词
添加/插入/创建/初始化/加载 add、append、insert、create、initialize、load
删除/销毁 delete、remove、destroy、drop
打开/开始/启动 open、start
关闭/停止 close、stop
获取/读取/查找/查询 get、fetch、acquire、read、search、find、query
设置/重置/放入/写入/释放/刷新 set、reset、put、write、release、refresh
发送/推送 send、push
接收/拉取 receive、pull
提交/撤销/取消 submit、cancel
收集/采集/选取/选择 collect、pick、select
提取/解析 sub、extract、parse
编码/解码 encode、decode
填充/打包/压缩 fill、pack、compress
清空/拆包/解压 flush、clear、unpack、decompress
增加/减少 increase、decrease、reduce
分隔/拼接 split、join、concat
过滤/校验/检测 filter、valid、check
  • 名词要专业

动词决定了方法的具体动作,而名词决定了方法具体的操作对象,对于名词,尽量使用领域词汇,其实常用的一共也没多少个词语,特别是不要去百度翻译一些生僻词,实在是没必要。还一点就是尽量遵循大家的习惯,比如集合长度一般用size,数组和字符串一般用length,那你就不要另辟蹊径用size表示字符串的长度。下面附一张名词表。

类别 单词
容量/大小/长度 capacity、size、length
实例/上下文 instance、context
配置 config、settings
头部/前面/前一个/第一个 header、front、previous、first
尾部/后面/后一个/最后一个 tail、back、next、last
区间/区域/某一部分/范围/规模 range、interval、region、area、section、scope、scale
缓存/缓冲/会话 cache、buffer、session
本地/局部/全局 local、global
成员/元素 member、element
菜单/列表 menu、list
源/目标 source、destination、target
  • 单一职责

java8支持lambda以后,有点想搞FP范式面向函数式编程,看现在操作集合使用stream里面的算子都行云流水操作,你只要记住算子的名字不需要点到里面具体的实现,你就知道它在做什么。java也非常推崇这样的方式,就是具体的操作你封装到方法里面,你取个好名字就行了。因为往往我们有公共代码才抽提方法,现在只要是一个单独的逻辑都可以提出来,然后主干上只写方法的调用,然后看你代码的人,很快就可以知道你再在做什么。

这里面最重要的就是方法的划分和不要干和方法名不相符的逻辑。不能一个方法里面塞一塞处理这个又处理那个,后期维护的时候想当然就什么东西都往里面塞。这样是接手人的噩梦。

  • 善用DTO

比如平时写CURD代码,查询学生接口,产品刚开始说希望通过年龄查,getStudentByAge;后面新需求又希望通过年龄和城市定位,getStudnetByAgeAndCity;然后又来了说希望能通过年龄、城市和性别定位,getS…停!如果这样的命名,那你的噩梦就开始了。

我觉得比较好的做法是,如果是通过主键查询那么可以使用By来连接查询,但是如果是通过其他属性并且后面会存在多个组合查询的可能性,那么建议进行封装getStudents(StudentSearchDto searchParam);

最后

最后,建议大家平时在写代码过程中,不要怕在命名上耗费时间也不要怕浪费时间进行总结,好的命名可以减少后期很多重构工作,所以答应我做到命名”见名知意”好吗?

(转载本站文章请注明作者和出处 没有气的汽水



┌┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┐
├ 文章已经完啦, 想要第一时间收到文章更新可以关注↓ ┤
└┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┘

Post Directory






下面是评论区,欢迎大家留言探讨或者指出错误哈