《码处高效:Java开发手册》之代码风格

流水淡,碧天长,鸿雁成行。编码风格,简捷清爽,反引无限风光。

在美剧《硅谷》中有这样一个经典镜头,主人公 Richard 与同为开发工程师的女友闹分手,理由是两人对缩进方式有着截然不同的编程习惯,互相鄙视对方的代码风格。Richard 认为" one tab saves four spaces “,缩进使用 Tab 键操作更快,更节省存 储空间,而女友坚持使用空格缩进,连续四次敲击空格的声音,把 Richard 折磨到几近崩溃,认为这是种精神折磨。 Richard 觉得难以相处,吵完架下楼梯时,不小心摔倒了 还淡定地说,”I just tried to go down the stairs four steps at a time ” (这只是表达我的立场而已)。Tab 键和空恪键的争议在现实编程中确实存在。除此之外,在其他代码风格上,也存在不同的处理方式,往往是谁也说服不了谁,都站在自身”完全正确”的立场上,试图说服对方。这在团队开发效率上,往往是一个巨大的内耗,无休止的争论与最后的收益是成反比的。所以我们认为一致性很重要,就像交通规则一样,我国规定靠右行驶,有些国家则规定靠左行驶,并没有绝对的优劣之分,但是在同一个国家或地区内必须要有统一的标准。代码风格也是如此,无论选择哪一种处理方式,都需要部分人牺牲小我,成就大我,切实提升团队的研发效能。

代码风格并不影响程序运行,没有潜在的故障风险,通常与数据结构、逻辑表达无关,是指不可见字符的展示方式、代码元素的命名方式和代码注释风格等。比如,大括号是否换行、缩进方式、常量与变量的命名方式、注释是否统一放置在代码上方等。代码风格的主要诉求是清爽统一、便于阅读和维护。统一的代码风格可以让开发工程师们没有严重的代码心理壁垒,每个人都可以轻松地阅读并快速理解代码逻辑,便于高效协作,逐步形成团队的代码”味道”。

命名规约

代码元素包括类、方法、参数、常量、变量等程序中的各种要素。合适的命名,可以体现出元素的特征、职责 ,以及元素之间的差异性和协同性。为了统一代码风格,元素的命名要遵守以下约定。

​ 什么是常量?常量是在作用域内保持不变的值, 一般用 final 关键字进行修饰,根据作用域区分,分为全局常量、类内常量、局部常量。全局常量是指类的公开静态属性 使用 public static final 修饰;类内常量是私有静态属性,使用 private static final 修饰,局部常量分为方法常量和参数常量,前者是在方法或代码块内定义的常量,后者是在定义形式参数时 增加 final 表示此参数值不能被修改。全局常量和类内常量是最主要的常量表现形式,它们的命名方式比较特殊,采用字母全部大写、单词之间加下画线的方式。而局部常量采用小驼峰形式即可。示例代码如下:

public class Constant {
    public static final String GLOBAL CONSTANT = "shared in global";
    private static final String CLASS CONSTANT = "shared in class";

    public void f(String a) {
        final String methodConstant = "shared in method";
    }

    public void g( final int b) {
        // 编译出错,不允许对常量参数进行重新赋值
        b = 3;
    }
}

​ 常量在代码中具有穿透性,使用甚广。如果没有一个恰当的命名,就会给代码阅读带来沉重的负担,甚至影响对主干逻辑的理解。首当其冲的问题就是到处使用魔法值。魔法值即”共识层面”上的常量,直接以具体的数值或者字符出现在代码中。这些不知所云的魔法值极大地影响了代码的可读性和可维护性。下面先来看一段实际业务代码。

public void getOnlinePackageCourse(Long packageId, Long userId) {
    if (packageId == 3) {
        logger.error("线下课程,无法在线观看");
        return;
    }
    // 其它逻辑处理
    PackageCourse online = packageService.getByTeacherId(userId);
    if (online.getPackageId() == 2) {
        logger.error("未审核课程");
        return;
    }
    // 其他逻将处理
}

​ 以上示例代码中,信手拈来的2和3分别表示未审核课程和线下课程,仅仅是两个数字,似乎很容易记忆。但事实上除2和3两种状态外,还有1、4、5分别代表新建、审核未通过、审核通过。在团队规模较小时,口口相传,倒也勉强能够记住这五个数字的含义,早期还有零星的注释,驾轻就熟的情况下,连注释也省了。现实是残酷的,团队迅速扩大后,课程状态个数也在逐步增加,新来的开发工程师在上线新功能模块时,把”审核通过”和”未审核课程”对应的数字搞反了,使得课程展示错误,导致用户大量投诉。随着应用变得越来越复杂,这些魔法值几乎成了整个后台服务代码中的梦魔。团队架构师终于下定决心进行系统重构,把这些魔法值以合适的命名方式定义成全局常量。使用 Enum 枚举类来定义课程类型,示例代码如下:

public enum CourseTypeEnurn {
    /**
     * 允许官方和讲师创建和运营
     */
    VIDEO_COURSE(l, "录插课程"),
    /**
     * 只允许官方创建和运营,初始化必须设置合理的报名人数上限
     */
    LIVE_COURSE(2, "直播课程"),
    /**
     * 只允许官方创建和运营
     */
    OFFLINE_COURSE(3,"线下课程");

    private int seq;
    private String desc ;

    CourseTypeEnurn (int seq, String desc) {
        this.seq = seq;
        this. desc = desc ;
    }

    public int getSeq() {
        return seq;
    }

    public String getDesc() {
        return desc;
    }
}

​ 上述示例代码把课程类型分成三种:录播课程、直播课程、线下课程。枚举类型几乎是固定不变的全局常量,使用频率高、范围广,所以枚举常量都需要添加清晰的注释,比如业务相关信息或注意事项等。再把课程状态分为新课程、未审核课程、审核通过、审核未通过、已删除五种状态。考虑到后续课程状态还会再追加,并且状态没有扩展信息,所以用不能实例化的抽象类的全局常量来表示课程状态,示例代码如

public abstract class BaseCourseState {
    public static final int NEW_COURSE = 1;
    public static final int UNAUTHED_COURSE = 2;
    public static final int PASSED_COURSE = 3;
    public static final int NOT_PASSED_COURSE = 4;
    public static final int DELETED_COURSE = 5;
}

使用重构后的常量修改原有的魔法值,对比一下代码的可读性

public void getOnlinePackageCourse(Long packageId, Long userId) {
    if (packageId == CourseTypeEnum.OFFLINE_COURSE.getSeq()) {
        logger.error("线下课程,无法在线观看");
        return;
    }
    // 其它逻辑处理
    VideoCourse course = packageService.getByTeacherId(userId);
    if (course.getState() == BaseCourseState.UNAUTHED_COURSE) {
        logger.error("未审核课程");
        return;
    }
    // 其他逻将处理
}

​ 我们认为,系统成长到某个阶段后,重构是种必然选择。优秀的架构设计不是去阻止未来切重构的可能性,毕竟技术枝、业务方向和规模都在不断变化,而是尽可能让重构来得晚一些,重构幅度小一些。即使类内常量和局部常量当前只使用一次,也需要赋予一个有意义的名称,目的有两个:第一、望文知义,方便理解 第二、后期多次使用时能够保证值出同源。因此,无论如何都不允许任何魔法值直接出现在代码中,避免魔法值随意使用导致取值不一致,特别是对于字符串常量来说,应避免没有预先定义,就直接使用魔法值。所谓常 在河边走,哪有不湿鞋,在反复的复制与粘贴后,难免会出现问题,警示代码如下:

String key = "Id#taobao_" + tradeId;
cache.put(key, value);

​ 上述代码是保存信息到缓存中的方法,即使用魔法值组装 Key。这就导致各个调用方到处复制和粘贴字符串 Id#taobao_ 这样似乎很合理。但某一天,某个粗心的程序员把Id#taobao_ 复制成为Id#taobao,少了下画线。这个错误在测试过程中,并不容易被发现 因为没有命中缓存,会自动访问数据库。但在大促时,数据库压力急剧上升,进而发现缓存全部失效,导致连接占满,查询变慢。小处不小,再次说明魔法值害人害己。

​ 某些公认的字面常量是不需要预先定义的,如 for( int i=0; … )这里的0是可以直接使用的。true和 false也可以直接使用,但是如果具备了特殊的含义,就必须定义出有意义的常量名称,比如在 TreeMap 源码中,表示红黑树节点颜色的 true 和 false 就被定义成为类内常量,以方便理解∶

private static final boolean RED = false;
private static final boolean BLACK = true;

​ 常量命名应该全部大写,单词间用下画线隔开,力求语义表达完整清楚,不要嫌名字长,比如,把最大库存数量命名为 MAX_STOCK_COUNT,把缓存失效时间命名为 CACHE_EXPIRED TIME。

​ 什么是变量从广义来说,在程序中变量是一切通过分配内存并赋值的量,分为不可变量(常量)和可变变量。从狭义来说,变量仅指在程序运行过程中可以改变其值的量,包括成员变量和局部可变变量等。

​ 一般情况下,变量的命名需要满足小驼峰格式,命名体现业务含义即可。存在一种特殊情况,在定义类成员变量时,特别是在 POJO类中,针对布尔类型的变量,命名不要加 is 前缀,否则部分框架解析会引起序列化错误。例如,定义标识是否删除的成员变量为 Boolean isDeleted,它的 getter 方法也是 isDeleted(),框架在反向解析的时候,”误以为”对应的属性名称是 deleted,导致获取不到属性,进而抛出异常。但是在数据库建表中,推荐表达是与否的值采用 is_xxx 的命名方式,针对此种情况,需要在

代码展示风格

缩进、空格与空行造就了代码的层次性和规律性,有助于直观、快速、准确地理解业务逻辑。没有缩进、空格和空行的代码可读性极差。如下反例所示∶

table=newTab;
if (oldTab!=null){ for(int j=0;j=0){
for(int i=0;i

代码中需要限定每行的字符个数,以便适配显示器的宽度,以及方便CodeReview时进行 diff 比对。对于无节制的行数字符,需要不断地拉取左右滚动条或者键盘移动光标,那是多么差的体验。因此,约定单行字符数不超过120个,超出则需要换行,换行时遵循如下原则∶
(1)第二行相对第一行缩进4个空格,从第三行开始,不再继续缩进,参考示例。

(2)运算符与下文一起换行。

(3)方法调用的点符号与下文一起换行。

(4)方法调用中的多个参数需要换行时,在逗号后换行。

(5)在括号前不要换行。

StringBuffer sb = new StringBuffer();
// 超过120个字符的情况下,换行缩进 4个空格,并且方法前的点号一起换行
sb.append ("ma").append("chu")...

    .append ("gao")...

    .append ("xiao")...

    .append("yealh");

代码注释

注释是一个看起来简单,容易被忽视,但是作用又不容小觑的话题。好的注释能起到指路明灯、拨云见日、警示等作用,具体包括∶能够准确反映设计思想和代码逻辑;能够描述业务含义,使其他工程师能迅速了解背景知识。与代码不同,注释没有语法的限制,完全取决于编写者的能力和发挥,但这并不意味着注释可以天马行空。书写注释要满足优雅注释三要素。

注释格式主要分为两种∶ 一种是 Javadoc 规范,另一种是简单注释。

​ 类、类属性和类方法的注释必须遵循Javadoc规范,使用文档注释(/***/)的格式。按 Javadoc 规范编写的注释,可以生成规范的 JavaAPI 文档,为外部用户提供非常有效的文档支持。而且在使用IDE 工具编码时,IDE 会自动提示所用到的类、方法等注释,提高了编码的效率。

​ 这里要特别强调对枚举的注释是必需的。有人觉得枚举通常带了String name 属性,已经简要地说明了这个枚举属性值的意思,此时注释是多余的。其实不然,因为∶

​ (1)枚举实在太特殊了。它的代码极为稳定。如果它的定义和使用出现错误,通常影响较大。

​ (2)注释的内容不仅限于解释属性值的含义,还可以包括注意事项、业务逻辑。如果在原有枚举类上新增或修改一个属性值,还需要加上创建和修改时间,让使用者零成本地知道这个枚举类的所有意图。

​ (3)枚举类的删除或者修改都存在很大的风险。不可直接删除过时属性,需要标注为过时,同时注释说明过时的逻辑考虑和业务背景。

​ 包括单行注释和多行注释。特别强调此类注释不允许写在代码后方,必须写在代码上方,这是为了避免注释的参差不齐,导致代码版式混乱。双画线注释往往使用在方法内部,此时的注释是提供给程序开发者、维护者和关注方法细节的调用者查看的。因此,注释的作用更应该是画龙点睛的,通常添加在非常必要的地方,例如复杂算法或需要警示的特殊业务场景等。

说明:本文内容参考《码出高效:Java开发手册》第三章 代码风格,有兴趣的读者可以看书的原文。

Original: https://www.cnblogs.com/reminis/p/16120110.html
Author: 小懒编程日记
Title: 《码处高效:Java开发手册》之代码风格

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/571478/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 超越iTerm! 号称下一代终端神器,功能贼强大!

    程序员的一生,用的最多的两个工具,一个是代码编辑器(Code Editor),另外一个就是命令行终端工具(Terminal)。这两个工具对于提高开发效率至关重要。 代码编辑器在过去…

    Java 2023年6月9日
    069
  • 1 系统功能

    系统功能 1、定位支持 描述:接收定位终端数据,实时定位并回传给定位终端 2、定位精度分析 描述:比较定位系统结果与真实值,给出定位精度报告 3、定位结果记录与分析 描述:保存定位…

    Java 2023年6月5日
    052
  • tcp 连接 time-wait 状态过多问题解释

    前言 两条竖线分别是表示: 主动关闭(active close)的一方 被动关闭(passive close)的一方 网络上类似的图有很多,但是有的细节不够,有的存在误导。有的会把…

    Java 2023年6月7日
    0109
  • Java基础-续1

    方法 概念 在JS中,我们把方法称之为函数。在java我们称之为方法。 方法就是一个黑匣子。我们不需要知道内部是如何执行的,只要按照要求调用,就能完成必要的功能。 方法申明的语法:…

    Java 2023年6月7日
    064
  • Mybatis源码5 StatementHandler ,ParameterHandler

    Mybatis5 StatementHandler ,ParameterHandler 一丶概述 前面我们总结了SqlSession—>CachingExecut…

    Java 2023年6月14日
    075
  • spring cloud Eureka server 问题 Spring Cloud java.lang.TypeNotPresentException

    版本: spring-cloud.version : Greenwich.SR2 pom配置: 1 2 xmlns:xsi="http://www.w3.org/2001…

    Java 2023年5月29日
    085
  • SQL(三)DDL、DML、DCL总结

    表的创建 create table 表名( 字段名1 数据类型, 字段名2 数据类型, 字段名3 数据类型 ); 创建student表并指定性别默认为男: create table…

    Java 2023年6月16日
    086
  • spring boot 中英文官方文档

    中文文档:http://oopsguy.com/documents/springboot-docs/1.5.4/index.html#boot-features 英文官方文档:ht…

    Java 2023年6月13日
    094
  • 分布式ID详解(5种分布式ID生成方案)

    分布式架构会涉及到分布式全局唯一ID的生成,今天我就来详解分布式全局唯一ID,以及分布式全局唯一ID的实现方案@mikechen 什么是分布式系统唯一ID 在复杂分布式系统中,往往…

    Java 2023年6月15日
    076
  • Vue学习之——–深入理解Vuex之模块化编码(2022/9/4)

    在以下文章的基础上1、深入理解Vuex、原理详解、实战应用:https://blog.csdn.net/weixin_43304253/article/details/126651…

    Java 2023年6月14日
    096
  • Spring Security 源码学习(二): Spring Security自动配置(初始化流程)

    【深度好文】: 「和耳朵」SpringSecurity是如何代理过滤器链的? 1. 自动配置security的bean信息 SpringBoot自动配置实现原理 下面是 Sprin…

    Java 2023年5月30日
    092
  • InnoDB学习(二)之ChangeBuffer

    ChangeBuffer是InnoDB缓存区的一种特殊的数据结构,当用户执行SQL对非唯一索引进行更改时,如果索引对应的数据页不在缓存中时,InnoDB不会直接加载磁盘数据到缓存数…

    Java 2023年6月8日
    077
  • java并发实战:连接池实现

    池化技术简介 在我们使用数据库的过程中,我们往往使用数据库连接池而不是直接使用数据库连接进行操作,这是因为每一个数据库连接的创建和销毁的代价是昂贵的,而池化技术则预先创建了资源,这…

    Java 2023年5月29日
    083
  • HIT软构博客6–设计模式

    设计模式可以分为1.创建型模式 2.结构型模式 3.行为类模式 1.创建型模式 ​ 工厂方法模式 ​ 当client不知道要创建哪个类的具体实例或不想在客户端代码指明要创建具体哪个…

    Java 2023年6月5日
    078
  • Junit执行单元测试用例成功,mvn test却失败的解决方法

    Junit执行单元测试用例成功,mvn test却失败的解决方法(具体体现为使用的H2数据库中mock进去的数据在Junit执行的时候可以访问到,但是mvn test的时候数据库却…

    Java 2023年6月13日
    064
  • 分页SQL语句的性能比较

    数据量暂时在10来万,排序字段是几个索引里面的一个索引的二级索引第一方案是比较传统的,第二方案用了表变量,有说数据量大的时候应该用临时表,不知道这个量要多少。感觉。。 DECLAR…

    Java 2023年6月14日
    065
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球