要写出能够运行的代码是比较容易的,但是要写出优雅的、逻辑简单直接、换个人接手不容易出错的代码是比较难的。一方面其实是靠程序员单兵的代码水平,但是现实情况就是一个团队的同学对代码的认识和追求就是参差不齐,而且在国内,我遇到的团队中,大概率大部分追求的是可运行的代码,不太会真的去care的代码的质量的。亲身经历的告诉我,大阿里的技术专家几乎都是非常功利主义的编程,拿结果、完成所谓的kpi才是王道。但另一方面,其实是可以通过规约的方式,拉齐大家的认知,减少这种错误。比如早期的google就有java的编程规范:Google Java Style Guide、中文翻译说明 | Google Java 编程规范(中文版)。包括后面阿里也推出了java编程规范,由于阿里在国内的影响力,以及强大的推销能力,阿里的java编程规范几乎成了javaer人尽皆知,包括我也是粉丝之一,翻阅了好几遍,包括后面持续的更新的版本,也都去翻阅过,直到华山版(后面作者离职了)
下面结合一些大家耳熟能详的一些编程规范资料,结合自己的经历,来总结、整理一些规范的补充。比如《重构-改善既有代码的设计》、《clean code》、《effect java》、《阿里的java变成规约》、网络博客等。其实主要集中在rest接口设计和分层上(其实主要是有一段经历这两个做的特别不好)
这些规约能不能顺利落实,其实主要看团队的氛围,这些东西都是一看都会,但是很容易一干就废,如果再加上团队的氛围是那种急功近利的氛围,那这个规约就是一个txt,没有任何价值的。
rest接口相关
【强制】url的路径上,不要使用pathVariable、不要使用param去路由Controller。url路径使用固定值,且仅仅通过path去路由Controller
原因:路径上不用pathVariable、不用param路由对rest监控更加友好,可以直接通过path来进行聚合统计。而且对接口维护也更友好,通过抓包更容易找到具体调用的那个接口。
例子:
- /appName/customer/{customerId}/detail,不ok。
/appName/customer/getCustomerDetai?customerId=xx更ok
ps:有一种场景,pathVariable是合适的,那就是接口需要版本的时候,这个使用pathVariable是可以的,这在APP的后端是比较常见的,但纯支持web后端的rest接口,完全没有必要。 - @RequestMapping(value = "/search", params = "type=customerByDB", method = RequestMethod.GET) 不OK。
@RequestMapping(value = "/searchCustomerFronmDB", method = RequestMethod.GET)
【强烈建议】url的路径不要超过5级。且路径中最好只使用英文字母,最多加上数字。不要用特殊字符。接口的最后一级是一个表达具体动作的词。比如分页用pageGetXXX、单个查询用getXXX、批量查询用batchGetXXX、新增用insertXXX/addXXX、修改用updateXXXX
ps:url的本质其实是资源定位,所以其实不建议在url带有操作性质的字眼,比如getxxx、updatexxx等,具体的操作用http的请求方式来体现。不过实际在web项目使用中,rest服务的url其实好多时候更多是一个接口的签名(其本质还是资源定位,不过定位的资源是一个服务)。所以这个时候,个人认为按照接口的命名规则去命名url,更加简介直观。简单粗暴的理解就是:将url看成一个具有分级的接口命名方式。
举个栗子:
【强制】rest接口的请求方式统一都使用POST和GET:写入(增删改)用POST、查询用GET,POST请求参数统一放body中、GET请求参数统一放request。
原因:对统一入口日志更加友好,维护的时候也不用纠结http规范中规定的各个请求方式的语义。
【强制】rest接口的返回值统一格式,被广泛使用的一个统一格式:
{
"data": {},
"code": 200,
"msg": ”ok“
}
【强烈建议】在Controller的@RequestMapping方法中,value值带上/。
@RequestMapping(value = "search"),不ok,更好的方式@RequestMapping(value = "/search")
好处:好多时候java方法名会和rest接口的末级路径同名,所以使用idea搜索的时候会搜索出很多,但是都带上/,就能精确的定位到Controller
【强烈建议】出入参,按需设计。避免为了而复用,导致出入参大量的无用参数传递。比如xxAllVo
重复度几乎是每个静态代码工具都会扫描的一个指标,而且复用几乎是所有程序员入门学习的代码精进的毕竟之路,复用的目的其实就是相同逻辑不要copy多份,当需要修改的时候也只需要修改一处,避免修改遗漏。但复用从另外一个视角就是耦合,耦合的问题就是不够灵活,迭代困难。所以说到复用性,一个面向业务的开发和基础中间件的开发应该是有不同的理解:
- 中间件开发中,其逻辑不应该和具体的业务有什么相关性,所以它更多的是通用的。所以代码重复度其实比较能够反应代码的质量
- 面向业务的开发,就不能只是去看代码重复度了,需要根据业务场景,从服务的业务上来看是不是重复的,应该复用。
举个栗子:两个接口,在某次迭代中他们百分之六十的字段和代码都是重复的,但是从业务上,这两个接口服务的是完全不同的业务场景,那么后续这两个业务场景分开得带的可能性非常大,那么这个时候虽然重复度达到了百分之六十,那么应该毫不犹豫的让他重复,而不是将这百分之60的重复代码给归置到一处。
ps:这里关于接口涉及其实是有个矛盾点的,需要有个取舍,如果取舍不当,不管左倾还是右倾,都会带来不好的结果
- 按照单一职责,接口的设计上,应该保持单一的职责,不应该有一些大接口出现。但是实际生产中,特别是历史老系统,按着这种思路对外暴露了一堆接口,这样就会有接口爆炸的问题
- 出现了接口爆炸,其实就会需要对这些接口进行一些治理,否则很难管理。治理接口爆炸的问题一个常用手段就是按需取数。简单粗暴点就是:设计一个接口,然后增加一个option参数,返回值和option参数对应,只是返回对应option参数=true的数据。从而达到接口爆炸治理的目睹。那其实就很明显,这个接口就不符合单一职责了,一个接口里累加了很多功能。但是如果带上option参数看,其实又是非常清晰的,所以这也是这个方式被很多地方推荐为best practice的原因。
所以,在查询接口上,一些复杂场景,谨慎的设计按需接口,其实是会有很好的效果的。但是千万别为了复用而复用option接口的返回值,入参没有了option,使用这是很难get到你哪些信息返回是有保证的。但是在修改接口上,还是不建议使用大接口。如果说一个应用出现了修改接口的爆炸,那说明应用划分已经不合理了,更好的方向是做应用拆分。
【强烈建议】rest接口特殊入参规约
- 对于枚举。最终给前端返回code+desc两个字段:当前端有不得不做的逻辑的时候需要感知枚举且使用code来做逻辑、以及回传的时候回传code;如果只是需要展示,直接展示desc
ps:枚举常量可选返回tips,用于解释对应枚举值的含义。 - 对于日期类型,返回时间戳,由前端决定展示格式(这个不一定,也可按照固定的格式交互,但是这种格式必须在一个一致的地方维护,保证展示格式的统一)
统一的规约的好处是对于这种通用的转换可使用通用的方式来做(springmvc的MessageConverter、或者ArgumentResolver、mybatis的拦截器),在业务逻辑中就不用关心数据格式了。
【强烈建议】Controller类的职责:出入参转换,不作任何业务逻辑。
ps:统一异常处理、水平鉴权等这种非常通用且和业务无关的,一般是在Controller前面通过拦截器实现
【强烈建议】Controller类的方法布局根据可见性由大到小,一次从上而下排版。即从上到下依次应该是:public方法-->protected方法-->包可见性方法-->private方法。这条规则其实对任何pojo类都是使用的。
原因:关注点聚焦/分离。
其他
【强烈建议】业务逻辑和值对象转换分开。
好处:焦点分离
举个栗子:
public void updateCustomerBasicInfo(CustomerBasicInfoVo customerBasicInfoVo) {
// 查询判断更新条件
// 值对象转换
CustomerDo customerDo2Update = new CustomerDo();
customerDo2Update.setCustomerName(customerBasicInfoVo.getCustomerName());
customerDo2Update.setCustomerId(customerBasicInfoVo.getCustomerId());
customerDo2Update.setStatus(customerBasicInfoVo.getStatus());
customerDo2Update.setCustomerDesc(customerBasicInfoVo.getCustomerDesc());
customerDo2Update.setCustomerMobileNo(customerBasicInfoVo.getCustomerMobileNo);
customerDo2Update.setCustomerResponsBd(customerBasicInfoVo.getCustomerResponsBd());
customerDo2Update.setCustomerPoiPicturlUrl(customerBasicInfoVo.getCustomerPoiPicturlUrl());
customerDo2Update.setLastModifier(OperatorHolder.getOperator().getAccount());
// 执行更新
customerConfigService.updateSelectiveByPdCode(customerDo2Update);
}
正栗:
public void updateCustomerBasicInfo(CustomerBasicInfoVo customerBasicInfoVo){
// 查询判断更新条件
// 执行更新
customerConfigService.updateSelectiveByPdCode(CustomerDoConverter.convert2CustomerDo(customerBasicInfoVo));
}
好处:一眼就能看出来更新后天产品基本信息做了写啥:先判断更新条件,然后执行更新。至于想要更多的值对象的转换细节,跳转到Converter去就好了。揉在一起的写法的问题就是:需要在n多代码中,找自己最关心的主要的业务逻辑,当逻辑比较复杂的时候,那么这个方法将达到n屏都放不下(best practice:保持一个方法的长度在一屏内可以展示)
【强烈建议】 所有日志中都必须打印traceId,且这个可以通过日志框架提供的能力来支持,不要依赖在业务逻辑日志打印的时候去写入。对于log4j2的MDC是可以做到这个事情的。
ps:关于日志其实还是想多说依据,一个好的日志中心,真的是事半功倍呀。而且日志方案其实是比较成熟的了,比如elk之类的,就怕有些大厂(某行业的互联网独角兽)非要自己去创新但是又搞不好,搞了个所谓的日志中心,其实就是抓取了磁盘日志,然后单独存储了,但是查询的时候还需要指定日志文件,但是又自己搞了一堆所谓的日志隔离,一个应用几十个日志文件,而且隔离的规范也没做好,中间件的日志traceId好多还是没有,线上排查真的是没有比这更难受和麻烦的
项目代码分层相关
阿里java编程规范的分层建议(图片也来自阿里java变成规范的截图)
- 在我遇到的好多项目中,manager和service要调个个,manager是靠近外层的,专注于业务逻辑;service层是整合层,一些通用公用的逻辑沉淀到service层。
- 所以service是一个非必选层,这是合理的,少一层少一些开销。不过对于mng系统来说,一个通俗简介的分层规则更重要些,所以service变成必选,个人认为更好(可讨论)
ddd分层建议:
综合下来看,一个应用的4层分层模型(在微服务的场景下,一个应用其实不会也不应该承担太多的功能,其实可以去掉service那一层)
总的调用关系:
- 禁止同层调用。
- 禁止下层调用上层
分层下的值对象的使用
- DO(Data Object):和数据库表对应,通过 DAO 层向上传输数据源对象。注意:这里不是一一对应,同一张表可以有多个DO与之对应,满足不同的场景,避免大sql。
ps:也有的定义PO(persistence Object),用处和DO是完全一样的。 - DTO(Data Transfer Object):数据传输对象。[Service 或 Manager 向外传输的对象,这个是阿里的编程规约中的描述],不过我讲过的更多的是通过rpc调用,通过网络传输的数据对象,使用DTO
- BO(Business Object):业务对象,可以由 Service 层输出的封装业务逻辑的对象
- VO(View Object):显示层对象,通常是 Web 向模板渲染引擎层传输的对象
ps:也有人将VO理解成value object,所以将
另外,除了这些耳熟能详的,
- 阿里编程规约中定义了xxxQuery ,它专门为各层的查询入参定义了一个对象,其核心点在于不要使用Map这种看不出任何业务语义的接口来作为插叙入参。
ps:我觉得这个触发点是没错的,不光是入参,返回值也不要用看不出任何韩含义的kv结构,而是应该定义出具有字描述的对象来充当入参和返回值,但是不感觉没必要专门为之定义一类对象。 - 我们特有的xxOrder,这个规定的来源,没有查到。从现有接口上看,xxxOrder是作为写入入参来封装待写入业务数据的,不过也有一些查询接口也在这么命名。
- xxOption。这个是治理接口爆炸提提的一个概念:按需取数,即将多个下接口合并成一个,然后增加一个xxOption来控制返回的数据,这样减少接口的数量。
- 关于常量:
- 枚举。后缀统一使用xxxEnum
- 使用类、interface管理的常量,后缀统一使用xxxConstant
执行不严格的分层规范:
- 服务调用关系强约束,上层只能调用下层提供的服务,不能同层调用、不能发现调用
是否可跨层调用,可讨论 - 各层交互使用的值对象,不强制限制每一层定义不同的值对象,跨层必须做转换。