一.前言
本文为《码出高效》系列博文第一篇,主要目的是统一和规范代码编程风格,改善应用程序的可读性,提高开发效率。规约包括命名、定义、函数、异常、排版等不同的场景,结合个人的实习经验和业界开发手册总结归纳,参考文档如下:
- 《阿里巴巴Java开发手册》:https://github.com/alibaba/p3c
- Alibaba Java Code Guidelines插件:https://github.com/alibaba/p3c/tree/master/idea-plugin
二.编码规范
1.系统分层
(1)分层架构设计
-
开放API接口层:可直接封装Service方法暴露成RPC接口;或通过Web封装成HTTP接口;进行网关安全控制、流量控制等。
-
WEB终端显示层:各个端的模板渲染并执行显示的层。当前主要包含velocity渲染,js渲染,JSP渲染,移动端展示等。
-
Controller层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等。
-
Service层:相对具体的业务逻辑服务层。
-
Manager层:通用业务处理层,它有如下特征:
-
对第三方平台封装的层,预处理返回结果及转化异常信息;
-
对Service层通用能力的下沉,如:缓存方案、中间件通用处理;
-
与DAO层交互,对多个DAO的组合复用;
-
-
DAO层:数据访问层,与底层MySQL、Oracle、Hbase等进行数据交互。
-
外部接口或第三方平台:包括其它部门RPC开放接口,基础平台,其它公司的HTTP接口。
(2)分层领域模型规约
- PO(Persistant Object):持久化数据对象,用于表示数据库中的一条记录映射成的java对象,与数据库表结构一一对应(与DO类似)。
- DO(Data Object):在alibaba开发手册中,此对象与数据库表结构一一对应,通过 DAO 层向上传输数据源对象(同PO)。在DDD领域驱动模型中,DO也可以称为Domain Object即领域对象(同BO)。
- DTO(Data Transfer Object):数据传输对象,Service 或 Manager 向外传输的对象。泛指用于展示层与服务层之间的数据传输对象。
- BO(Business Object):业务对象,可以由 Service 层输出的封装业务逻辑的对象。有的团队则当做 Service 层内保存中间信息数据的 “DTO” 或者上下文对象来使用(本文采用这种理解)。
- Query:数据查询对象,各层接收上层的查询请求。注意超过 2 个参数的查询封装,禁止使用 Map 类来传输。
- VO(View Object):显示层对象,通常是 Web 向模板渲染引擎层传输的对象。它的作用是把某个指定页面(或组件)的所有数据封装起来,通常控制层将其作为JSON 返回给前端然后前端渲染。
- POJO(Plain Ordinary Java Object):专指包括setter/getter/toString的简单类JavaBeans,包括DO/DTO/BO/VO等,但禁止直接命名成xxxPOJO。
- 大致示例代码:
-
Controller层:public List<UserVO> getUsers(UserQuery userQuery)。此层常见的转换为:DTO转VO
-
Service/Manager层:List<UserDTO> getUsers(UserQuery userQuery)。然后在Service内部使用UserBO封装中间所需的逻辑对象,此层常见的转换为:PO转DTO,或PO转BO转DTO
-
DAO层:List<UserPO> getUsers(UserQuery userQuery);
-
(3)分层视图
参考文章(写得很好):https://blog.51cto.com/knifeedge/5139389
- 查询视图
- 返回视图
(4)总结
有的朋友查询参数喜欢通过 Map 或者 JSONObject 来封装。有些朋友可能会认为这么多模型没有必要,因为通常各层模型的属性基本相同,而且各种类型的分层模型对象转换非常麻烦。使用不同的分层领域模型能够让程序更加健壮、更容易拓展,可以降低系统各层的耦合度。
分层模型的优势只有在系统较大时才体现得更加明显。设想一下如果我们不想定义 DTO 和 VO,直接将 DO 用到数据访问层、服务层、控制层和外部访问接口上。此时该表删除或则修改一个字段,DO 必须同步修改,这种修改将会影响到各层,这并不符合高内聚低耦合的原则。通过定义不同的 DTO 可以控制对不同系统暴露不同的属性,通过属性映射还可以实现具体的字段名称的隐藏。不同业务使用不同的模型,当一个业务发生变更需要修改字段时,不需要考虑对其它业务的影响,如果使用同一个对象则可能因为 “不敢乱改” 而产生很多不优雅的兼容性行为。
如果我们不愿意定义 Param 对象,使用 Map 来接收前端的参数,获取时如果采用 JSON 反序列化,则可能出现上一节所讲到的反序列化类型丢失问题。如果我们不使用 Query 对象而是 Map 对象来封装 DAO 的参数,设置和获取的 key 很可能因为粗心导致设置和获取时的 key 不一致而出现 BUG。
但总的来说,上面只是给出一种参考,很多团队对部分分层模型的理解会有差异,实际的使用过程中根据自己团队的规模可以适当变通。比如有很多团队项目并不是特别大,为了降低复杂度,只用到了 DTO 、VO 、DO 三种分层领域模型。
2.命名规范
级别 | 规则 | 备注 | |||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
强制 | 各个实体和类的命名规则
| ||||||||||||||||||
强制 | 类名使用 UpperCamelCase 风格,以下情形例外:DO / PO / DTO / BO / VO / UID 等 | ||||||||||||||||||
强制 | 接口类不要用I开头 | ||||||||||||||||||
强制 | 方法名、参数名、成员变量、局部变量都统一使用 lowerCamelCase 风格 | ||||||||||||||||||
强制 | 常量命名应该全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。 | ||||||||||||||||||
强制 | 包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用单数形式,但是类名如果有复数含义,类名可以使用复数形式。 | ||||||||||||||||||
强制 | 杜绝完全不规范的英文缩写,避免望文不知义。 | ||||||||||||||||||
强制 | 下层不依赖上层。 | ||||||||||||||||||
建议 | 命名应名副其实。变量、函数或类的名称应该告诉我们,它为什么会存在,它做什么事,应该怎么用。 | 两个反例: 一是数字系列。如a1,a2,a3..an。 二是废话。假设你有一个Product类,如果还有一个ProductInfo或ProductData类,那他们的名称虽然不同,意思却无区别。 | |||||||||||||||||
建议 | 类名和对象名应该是名词或名词短语。 | ||||||||||||||||||
建议 | 方法名应当是动词或动词短语。 |
3.常量定义
级别 | 规则 | 备注 |
---|---|---|
强制 | 不允许任何魔法值(即未经预先定义的常量)直接出现在代码中。 | if(status == 1) 改成 static final int VALID = 1; if(status == VALID) |
4.函数
级别 | 规则 | 备注 |
---|---|---|
强制 | 单行字符数限制不超过 120 个,超出需要换行。 | // 补充换行的推荐格式 |
强制 | 单个方法的总行数不超过60行。 | 不超过显示屏一屏 |
强制 | 函数的缩进不该多于2层。 | |
强制 | 方法内的变量声明应尽可能靠近其使用位置。 | |
建议 | if语句、else语句、while语句等,其中的代码块应该只有一行。该行应该是一个函数调用语句。 | |
建议 | 无副作用。函数承诺只做一件事,不应该还做其他隐藏的事。 | |
建议 | 分隔指令(写操作)与询问(读操作)。 | 函数要么做什么事,要么回答什么事,但二者不可得兼。函数应该修改某对象的状态,或是返回该对象的有关信息。两样都干常会导致混乱。 |
建议 | 类中自上向下展示函数调用依赖顺序。 | 被调用的函数应该尽可能放在执行调用的函数下面。 我们期望在阅读函数时,最重要的概念和流程先展示出来,用包含最少细节的方式表述它们。底层细节最后出来。 |
5.参数
级别 | 规则 | 备注 |
---|---|---|
强制 | 不要使用输出参数。 | 如果函数要对输入参数进行转换操作,转换结果就该体现为返回值。 |
建议 | 提供最小的参数集合。 | 反例:以10个字段的domain对象作为查询参数,但在函数内部只使用到了其中的3个字段。 |
建议 | 不在函数内部修改参数对象。 |
6.异常
级别 | 规则 | 备注 |
---|---|---|
强制 | 给出异常发生的环境说明 | 抛出的每个异常,都应当提供足够的环境说明。应创建信息充分的错误消息,并和异常一起传递出去。 |
强制 | 不使用大段的try catch。如果需要,应该在外层方法使用。 | |
强制 | 不要吞掉异常 | |
应用内部使用异常而非返回码 | 使用错误码的问题在于,调用者必须在调用之后立即检查错误。这个步骤很容易被遗忘。 从指令式函数返回错误码轻微违反了指令与询问分隔的规则。 当返回错误码时,就是在要求调用者立刻处理错误。另一方面,如果使用异常替代返回错误码,错误处理就能从主路径代码中分离出来,得到简化。 | |
建议 | thrift接口发生错误时,使用异常还是Result对象 | |
建议 | 多使用运行时异常,少使用受检异常。 | 受检异常的特点是调用方必须捕获并处理。适用场景:1.正常适用API也不能避免异常。2.一旦发生异常,程序可以立即采取有用的动作进行恢复。 受检异常违反开放闭合原则。如果在方法中抛出受检异常,而catch语句在三个层级之上,你就得在catch语句和抛出异常之间的每个方法签名中声明该异常。这意味着较低层级的修改,都将波及较高层级的签名。 |
7.类
级别 | 规则 | 备注 |
---|---|---|
强制 | 单一权责原则 | 类或模块应有且只有一条加以修改的理由。 系统应该由许多短小的类而不是少量巨大的类组成。每个小类封装一个权责,只有一个修改的原因,并与少数其他类一起协同达成期望的系统行为。 反例:CommonService |
建议 | 类应该从一组变量开始。公共函数应跟在变量列表之后。私有函数跟在公共函数后面。 |
8.OOP规约
级别 | 规则 | 备注 |
---|---|---|
强制 | 构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在 init 方法中。 | |
建议 | 定义 DO / PO / DTO / VO 等 POJO 类时,不要设定任何属性默认值。 |
9.控制语句
级别 | 规则 | 备注 |
---|---|---|
建议 | 不要在条件判断中包含复杂逻辑。将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性。 |
必须 | 对于if „ else if „(后续可能有多个else if …)这种类型的条件判断,最后必须包含一个else分支,避免出现分支遗漏造成错误;每个switch-case语句都必须保证有default,避免出现分支遗漏,造成错误 | |
建议 | 如果在分支中返回或中断,请尽量少用else分支,直接写成:if(true) {//do...; return;} 后边再写else逻辑 | |
必须 | 条件判断要先易后难,先简后繁,率先作无效推定。禁止深层的if嵌套导致的箭头式代码 |
10.注释规约
级别 | 规则 | 备注 |
---|---|---|
强制 | 所有的抽象方法(包括接口中的方法)必须要用 Javadoc 注释、除了返回值、参数异常说明 | |
强制 | 所有的枚举类型字段必须要有注释,说明每个数据项的用途。 | |
建议 | 与其用半吊子英文来注释,不如用中文注释说清楚。专有名词与关键字保持英文原文即可。 |
11.线程安全
序号 | 内容 | 要求 |
---|---|---|
1 | 使用Lock类API加锁成功后,需在finally中显式释放Lock | 强制 |
2 | 线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,从而能更好的控制core、max 线程数及workQueue的大小, workQueue默认情况下是整形最大值,应合理设置大小,避免出现内存撑满 | 强制 |
3 | 建议wait、await设置timeout时间,避免没有被notify一直阻塞 | 建议 |
4 | 使用CountDownLatch进行异步转同步操作,每个线程退出前必须调用countDown方法,线程执行代码注意catch异常,确保countDown方法被执行到,避免主线程无法执行至await方法,直到超时才返回结果 | 强制 |
5 | SimpleDateFormat 是线程不安全的类,一般不要定义为static变量。建议使用DateTimeFormatter | 建议 |
6 | 使用synchronized, lock等重量级锁是考虑是否用轻量级锁如AtomicXXX能实现。 一般写多,冲突多:用重量级锁 读多写少:用轻量级锁,或者读写锁 | 建议 |
12.代码设计
序号 | 内容 | 要求 |
---|---|---|
1 | 策略模式:当我们出现多种算法,需要自由切换时,可考虑策略模式 | 建议 |
2 | 尝试应用设计模式 代理模式:当系统出现隐藏或保护,控制目标对象,或是扩展目标对象能力,可考虑代理模式 | 建议 |
3 | 单一职责原则:不要设计大而全的接口,接口的功能单一而具体的。 | 建议 |
4 | 开闭原则。应尽可能按照开闭原则,对扩展开放,对修改封闭。
| 建议 |
5 | 里氏替换:子类可以扩展父类的功能,但不能改变父类原有的功能 | 建议 |
6 | 命令模式:当系统存在多个Action且Action中实现不一样时,可考虑引入命令模式 | 建议 |
7 | 依赖倒置原则:基于接口编程而非实现编程,当我们实现service的时候,应把业务能力抽象成service接口 | 建议 |
8 | 装饰模式:当系统存在想透明增强某个功能时,可考虑装饰模式 | 建议 |
9 | 状态模式:当系统出现多于3个以上状态时,考虑是否可以引入状态模式。状态的流转实现在每种状态中。 | 建议 |
13.其他
级别 | 规则 | 备注 |
---|---|---|
建议 | 减少重复代码 | 一旦有重复代码存在,阅读这些重复的代码时你就必须加倍仔细,留意其间细微的差异。如果要修改重复代码,你必须找出所有的副本来修改。 |
建议 | 不用sleep函数 | sleep的方案有以下几个问题:
如果是为了限制访问速率,推荐用专门的限流工具处理。 |
建议 | 不写没有业务特点的工具类 |