实习记录(一) Java 编程风格规约

一.前言

        本文为《码出高效》系列博文第一篇,主要目的是统一和规范代码编程风格,改善应用程序的可读性,提高开发效率。规约包括命名、定义、函数、异常、排版等不同的场景,结合个人的实习经验和业界开发手册总结归纳,参考文档如下:

  • 《阿里巴巴Java开发手册》:https://github.com/alibaba/p3c
  • Alibaba Java Code Guidelines插件:https://github.com/alibaba/p3c/tree/master/idea-plugin

二.编码规范

1.系统分层

(1)分层架构设计

实习记录(一) Java 编程风格规约_第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 getUsers(UserQuery userQuery)。此层常见的转换为:DTO转VO

    • Service/Manager层:List getUsers(UserQuery userQuery)。然后在Service内部使用UserBO封装中间所需的逻辑对象,此层常见的转换为:PO转DTO,或PO转BO转DTO

    • DAO层:List getUsers(UserQuery userQuery);

(3)分层视图

        ​​​​​​​参考文章(写得很好):https://blog.51cto.com/knifeedge/5139389​​​​​​​ 

  • 查询视图

实习记录(一) Java 编程风格规约_第2张图片

  • 返回视图

实习记录(一) Java 编程风格规约_第3张图片

 (4)总结

        有的朋友查询参数喜欢通过 Map 或者 JSONObject 来封装。有些朋友可能会认为这么多模型没有必要,因为通常各层模型的属性基本相同,而且各种类型的分层模型对象转换非常麻烦。使用不同的分层领域模型能够让程序更加健壮、更容易拓展,可以降低系统各层的耦合度。

        ​分层模型的优势只有在系统较大时才体现得更加明显​。设想一下如果我们不想定义 DTO 和 VO,直接将 DO 用到数据访问层、服务层、控制层和外部访问接口上。此时该表删除或则修改一个字段,DO 必须同步修改,这种修改将会影响到各层,这并不符合高内聚低耦合的原则。通过定义不同的 DTO 可以控制对不同系统暴露不同的属性,通过属性映射还可以实现具体的字段名称的隐藏。不同业务使用不同的模型,当一个业务发生变更需要修改字段时,不需要考虑对其它业务的影响,如果使用同一个对象则可能因为 “不敢乱改” 而产生很多不优雅的兼容性行为。

        如果我们不愿意定义 Param 对象,使用 Map 来接收前端的参数,获取时如果采用 JSON 反序列化,则可能出现上一节所讲到的反序列化类型丢失问题。如果我们不使用 Query 对象而是 Map 对象来封装 DAO 的参数,设置和获取的 key 很可能因为粗心导致设置和获取时的 key 不一致而出现 BUG。

        但总的来说,上面只是给出一种参考,很多团队对部分分层模型的理解会有差异,实际的使用过程中根据自己团队的规模可以适当变通。比如有很多团队项目并不是特别大,为了降低复杂度,只用到了 DTO 、VO 、DO 三种分层领域模型。

2.命名规范

级别

规则

备注

​强制

各个实体和类的命名规则

层级

实体

接口层

PersonController

PersonVO

PersonThriftService

PersonDTO

业务逻辑层

接口类:PersonService

实现类:PersonServiceImpl

PersonBO

领域层

PersonDomainService

PersonDO

持久层

PersonMapper

PersonPO

Enum命名规范:
业务名+Enum:PersonAcitveEnum

接口参数类命名规范:
业务名+Command:VoucherQueryCommand

内部参数类命名规范:
业务名+Param:VoucherQueryParam


result类命名规范:
业务名+Result:VoucherOperateResult

工具类命名规范:
针对内容+Utils:VoucherUtils,命名注意尽量不要和已有的utils重复(包括本地和三方库),避免出现误用。

抽象类命名使用 Abstract开头;

异常类命名使用 Exception 结尾

测试类命名以它要测试的类的名称开始,以 Test 结尾。
 
设计模式中统一名称后缀
Factory/Listener/Proxy/Observer/Builder/Handler等

​强制

类名使用 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的方案有以下几个问题:

  1. 处理时长变慢。

  2. 设置的时长难以确定

  3. 代码的影响范围难以评估。sleep代码在编写最初可能是为了解决一个简单的主从延迟场景问题。后来这段代码被多处依赖,维护者也频繁更换。最终新的维护者想要优化此处的处理时长时,既不敢删掉sleep代码,又不敢修改时间。因为他无法确定是否有其他的地方的代码是因为这里sleep几秒才正确运行。


如果是为了解决主从延迟的问题,推荐用强制读主库的方式处理。

如果是为了限制访问速率,推荐用专门的限流工具处理。

​建议

不写没有业务特点的工具类

你可能感兴趣的:(java,ide)