代码质量管理之规范篇
# 为何要进行代码质量管理
从实践经验上来看,代码质量管理主要达到以下目的:
- 可维护性:高质量的代码更易于维护和扩展。通过进行代码质量管理,你可以确保代码结构良好、易于理解,从而降低维护成本和项目风险。
- 可读性:高质量的代码具有良好的可读性,这有助于开发团队更快地理解和修改代码。代码质量管理有助于确保代码的一致性和可读性,提高团队协作效率。
- 可靠性和稳定性:通过进行代码质量管理,可以降低软件缺陷的数量,从而提高软件的可靠性和稳定性。这对于用户体验和满意度至关重要。
- 性能:代码质量管理有助于发现潜在的性能问题,从而提高软件的性能。良好的性能对于用户体验和应用程序的成功至关重要。
- 安全性:代码质量管理可以帮助你发现和修复潜在的安全漏洞,从而提高软件的安全性。安全性对于保护用户数据和防止恶意攻击至关重要。
- 开发效率:通过进行代码质量管理,你可以提高开发团队的效率,减少因低质量代码而导致的浪费时间和精力。
- 持续改进:代码质量管理有助于发现问题和改进点,从而促使开发团队不断改进软件质量。这有助于提高产品竞争力和满足客户需求。
- 团队协作和知识共享:代码质量管理有助于建立团队协作和知识共享的文化,通过代码审查和团队讨论来共同解决问题和提高代码质量。
那么反过来推算,不进行代码质量管理,就难免陷入维护性差、可读性差、可靠性差、性能差、安全性差、效率低下、团队合作困难等的泥沼。
如果进行了良好的代码质量管理,那么就能有效的避免代码中坏味道的发酵,可以让团队成员更加的peace,可以让世界更加的美好。 代码质量管理话题比较大,这篇文章先来谈谈代码规范。
# 应用架构规范
宏观的系统架构主要解决复杂性问题,那是一个庞大的话题。
这里主要谈一下应用架构。每个公司的应用架构可能有所不同,常见的有传统三层、整洁架构、领域驱动设计等。
而不管什么样的架构设计,核心原则是基本一致的:也就是以业务为核心,解耦外部依赖,分离业务复杂度和技术复杂度。
我以典型的ERP系统业务架构来举例,分层结构如下:
- Adapter、Web、OpenApi 包含了中断适配层、Web展示层、开发接口层
- Service 业务逻辑服务层
- Domain 领域层,借鉴了领域驱动设计的概念名称,也可以叫Manage层。Domain层的作用:沉淀复杂逻辑和公共方法
- Dao 持久层,与数据库交互
# 层级规范
- 不能跨层调用
- 下一层不能调用上一层
- 对于Adapter和Web来说,一个页面对应一个Controller。【弹性:大部分情况下是如此,如果一个页面包含强关联的子页面,则考虑放在一个Controller中即可】
- 一个Controller对应一个Service,不能出现一个Controller聚合多个Service的情况,最好不出现一个Service被多个Controller引用的情况
- 关于Domain层,主要用来沉淀复杂逻辑和公共方法
- Service层可以聚合Domain和Dao
- persistence层中的Dao方法尽量为原子性操作。有一些简单逻辑的方法,建议也写在持久层,便于复用
- 凡是涉及到事务,必须写在Service或Domain层,并且加上@Transactional(rollbackFor = Exception.class)注解
# 数据模型规范
- Entiry 数据实体类,和数据库表一一对应,普通Bean,不用固定后缀
- PO 持久化对象,数据库查询结果对应的对象
- Query(Qry) 读取命令
- Command(Cmd) 写入命令
- BO 业务对象,Service层输出的对象
- DO 领域对象,Domain层输出的对象
- DTO 数据传输对象,Adapter层输出的对象
- 入参和返回值均不允许出现Map类型
# package与类设计规范
- Service接口都以Api结尾,放在api包下
- Service实现类都以Service结尾,放在service包下
- Controller都以Controller结尾,放在controller包下
- Controller以一组页面为一个package,有自己独立的包名。对于ERP来说,一级菜单对应一个package,比如用户管理就在controller.usermanager下;对于Adapter来说,一组页面对应一个package,比如游客数据就在controller.tourist下,会员中心就在controller.buyermember下
- Service的包名结构:同一个业务体系下的Service有一个独立的package,比如erpweb、appadapter、pcweb;在系统级别以下,package对应controller的包名来建立。比如api.erpweb.usermanager和service.erpweb.usermanager
- 前后端分离项目中的Controller的返回值是DTO;前后端不分离的项目中使用MVC模式,Controller和View可以直接使用BO
- 一个Qry对应一个Service中的方法;一个Cmd对应一个逻辑处理类(CmdExe)。【弹性:复杂情况下用到CmdExe,这样利于解构和可读性。一般情况下,Cmd的实现可以放在Service中】
- Api的入参必须是Qry或Cmd。【弹性:只有一个参数或逻辑简单的情况下,可以直接用原始类型入参,但也不允许超过3个参数】
- Api的每个方法有独立的Cmd(Qry)和BO返回值,不允许复用Cmd(Qry)和BO。【弹性:如果有复用的数据模型,必须严格控制,写好注释】
- 所有Controller、Service、Domain、Dao对应的数据模型都应该放到所属接口中(作为内部类),不要单独放到package中。【弹性:一些共用的数据模型允许放到common package中,但要严格控制,写好注释】
备注:严格控制的意思是必须经过架构师的同意,否则代码提交会被驳回
# Java 代码规范
- package名必须全小写;类名以大写开头的驼峰命名;方法名、参数名、变量名以小写驼峰命名;常量、枚举以全大写+下划线方式命名
- Long类型必须以大写L结尾;Double类型必须以大写D结尾;Float类型必须以大写F结尾
- 不允许在程序任何地方中使用: 1)java.sql.Date 2)java.sql.Time 3)java.sql.Timestamp
- 代码格式统一使用Google Java Formater规范,IDEA上安装google-java-formater插件;每行最大为160,需要在IDEA中进行设置
- IDEA中安装Alibaba Java Coding Guidelines插件。【弹性:可以选择关闭某些检查,但要严格控制】
- IDEA中检查不允许关闭。【弹性:可以选择关闭某些检查,但要严格控制】
- 方法返回值建议都标注@Nullable和@NotNull,方便调用者处理空指针的问题
- 项目中有core基础包,所有的工具类都在其中沉淀,不允许自定义工具类,统一在core中进行管理。如果要使用新工具类,请找架构师
备注:
- 严格控制部分,反馈到架构师后,架构师假如公示列表中,并对每项予以说明
- core基础包要进行版本管理和发行说明
- core基础包最好要分包,比如core-poi、core-image、core-pdf等
进阶阅读:
# Mysql 规范
- 表达是与否概念的字段,必须使用 is_xxx 的方式命名,数据类型是 unsigned tinyint(1 表示是,0 表示否)
- 表名、字段名必须使用小写字母或数字
- 表名不使用复数名词
- 禁用保留字
- 主键索引名为 pk_字段名;唯一索引名为 uk_字段名;普通索引名则为 idx_字段名
- 小数类型为 decimal,禁止使用 float 和 double
- 如果有text类型的字段,独立出来一张表,用主键来对应,避免影响其它字段索引率
- 表必备三字段:id,create_time,update_time(或last_update_time)
- 在数据库中不能使用物理删除操作,要使用逻辑删除
- 查询字段必须建立索引
- 业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引
- 利用覆盖索引来进行查询操作,避免回表
- SQL 性能优化的目标:至少要达到 range 级别,要求是 ref 级别,如果可以是 const 最好
- 防止因字段类型不同造成的隐式转换,导致索引失效
- 不要使用 count(列名) 或 count(常量) 来替代 count(),count() 是 SQL92 定义的标准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关
- 不得使用外键与级联,一切外键概念必须在应用层解决
- 禁止使用存储过程,存储过程难以调试和扩展,更没有移植性
- 对于数据库中表记录的查询和变更,只要涉及多个表,都需要在列名前加表的别名(或表名)进行限定
- 所有的字符存储与表示,均采用 utf8mb4 字符集
扩展阅读:
# Redis 规范
- 使用合适的键名:为键选择具有描述性的名称,以便于理解和维护。控制键的长度,不超过256个字符
- 键名命名规范:建议使用冒号(:)作为命名空间的分隔符。例如,user:1:profile表示用户1的个人资料
- 键名不要包含特殊字符,只允许数字、字母、下划线、中划线、冒号
- 拒绝 bigkey(防止网卡流量、慢查询) string 类型控制在 10KB 以内,hash、list、set、zset 元素个数不要超过 5000
- 选择适合的数据类型。一般Java Bean,建议用string json存储;保存uv,建议用HyperLogLog;保存签到数据,建议用位图;解决推送重复的问题,可以用布隆过滤器;寻找附近的人,可以用GeoHash
- 控制 key 的生命周期,必须有过期时间,redis 不是垃圾桶。如果有特殊数据,需要长期存储,一定要放到单独的db中且经过架构师同意
- 避免使用“KEYS *”命令,使用“SCAN”命令替代,以避免性能问题
# RESTful(前后端交互) 规范
- 生产环境必须使用 HTTPS
- 每一个 API 需对应一个路径,表示 API 具体的请求地址。路径表示资源,用名词,推荐用复数,不能用动词(请求方式已经表示了动作)
- URL 路径不能使用大写,单词如果需要分隔,统一使用下划线
- 路径禁止携带表示请求内容类型的后缀,比如".json",".xml",通过 accept 头表达即可
- URL 带的参数必须无敏感信息或符合安全要求;body 里带参数时必须设置 Content-Type
- 最常用的请求方式。GET:从服务器取出资源。POST:在服务器新建一个资源。PUT:在服务器更新资源。DELETE:从服务器删除资源。
- 服务端发生错误时,返回给前端的响应信息必须包含 HTTP 状态码,errorCode、errorMessage、用户提示信息四个部分
- 在前后端交互的 JSON 格式数据中,所有的 key 必须为小写字母开始的驼峰命名风格,符合英文表达习惯,且表意完整
- HTTP 请求通过 URL 传递参数时,不能超过 2048 字节
- 在翻页场景中,用户输入参数的小于 1,则前端返回第一页参数给后端;后端发现用户输入的参数大于总页数,直接返回最后一页
- 所有的Adapter,入参都要使用@Valid注解校验
- 所有的Api方法,返回空对象或空数组,不要返回null。便于前端前端判断
- 所有Controller用@RestController注解,这样意味着类所有的方法都默认用@ResponseBody注解。所有的method推荐使用RESTFul注解,如@GetMapping、@PostMapping,而不要用@RequestMapping
- HTTP状态码是所有开发者的基本功,善用状态码。比如200/401/403/404/500。前端要对状态码做处理,给与用户友好提示,比如500是服务器走丢了,请稍后再试!状态码也包含逻辑,比如401是未授权或过期,需要用户重新登录
- 开发适配层或开放API的原则是:只新增,不修改。对于旧版本的api访问,返回404,并在前端提示用户版本过低
# Swagger 规范
- 对于适配层或开放API的Controller,要配合Swagger 3,代码清晰,也容易和Yapi集成
- 对于两个不同的DTO类,@ApiModel注解的value千万不要一样,否则接口会出现混乱的情况
- @ApiModel注解的类,不光要有value,最好也有description。这样可以在yapi中显示备注
# 测试规范
- 新手写业务代码,老手写测试代码
- 单元测试必须具备Automatic(自动化),Independent(独立性),Repeatable(可重复)性
- 单元测试要保证测试粒度足够小。单元测试测试粒度足够小,有助于精确定位问题。单测粒度至多是类级别,一般是方法级别
- 单元测试要遵守BCDE原则,Border,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等;Correct,正确的输入,并得到预期的结果;Design,与设计文档相结合,来编写单元测试;Error,强制错误信息输入(如:非法数据、异常流程、非业务允许输入等),并得到预期的结果
- core工具包测试代码覆盖率要90%以上
# 其他语言
上面的规范只是一个基础,对于其他语言的开发来说,也应该建立自己的开发规范和统一格式化要求。
以大前端举例,可能有如下规则:
- 优先使用TypeScript
- HTML标签名小写
- CSS使用统一的预处理器、命名方式以
-
作为分隔符 - 团队有统一的ESLint规则
# 代码精进之道(张建飞)
要记住,留给公司一个方便维护、整洁优雅的代码库,是我们技术人员最高技术使命,也是我们对公司做出的最大技术贡献。
【防止破窗】首先我们要有一套规范,并尽量遵守规范,不要做“打破第一扇窗”的人;其次,发现“破窗”要及时修复,不要让问题进一步恶化;
【三次原则】第一次用到某功能时,写一个特定的解决方法;第二次又用到时,复制上一次的代码;第三次出现时,就要着手写通用解决方案了;
【最小惊奇原则】写代码不是写侦探小说,要的是简单易懂,而不是时不时搞点烧脑的骚操作;
【日志规范】能用debug就不要用info,能用warn就不要用error。滥用的error与狼来了无疑;
【方法参数要少】参数越少,越容易理解,也便于测试,各个参数的组合就如笛卡尔积;
【空行规范】方法、逻辑分段,要加空行,提高代码可读性。车轮毂与车轴之间有空隙,车才能跑;书法绘画有留白;
【请求读写分离】增删改,会改变对象的状态,只需返回成功失败即可;查询是不会改变对象状态的,对系统没副作用。
# 总结
上面的规范在不同层次的开发者看来会有不同的感受。
初中级开发者可能会有很多的苦恼,一个是看不懂,一个是不理解,这都是正常的。作为一个有爱的团体,前辈应该积极的帮助后辈,在新手不理解的情况下,多给与他们指点方向。同时也希望新手们充满热情和好奇的去了解每一条规则背后的知识。
同时,对于走到一起的高级开发者来说,也会有一个适应的过程,如果你对自己够自信,那么欢迎你提出建议。这里要注意几个原则:1、没讨论并确定之前,不要轻动;2、讨论的时候可以激烈,定下来之后要严格执行;3、越厉害的人要越会倾听(架构师要倾听并尊重别人的意见)
祝你变得更强!