让你最快速地改善代码质量的 20 条编程规范

Posted 小羊子说

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了让你最快速地改善代码质量的 20 条编程规范相关的知识,希望对你有一定的参考价值。

根据学习部分极客时间 《设计模式之美》专栏 (王争 前Google工程师)和《阿里 java 规范》整理总结。

分别介绍编码规范的三个部分:命名与注释(Naming and Comments)、代码风格(Code Style)和编程技巧(Coding Tips)。

关于命名

  • 命名的关键是能准确达意。对于不同作用域的命名,我们可以适当地选择不同的长度。作用域小的变量(比如临时变量),可以适当地选择短一些的命名方式。除此之外,命名中也可以使用一些耳熟能详的缩写。
  • 我们可以借助类的信息来简化属性、函数的命名,利用函数的信息来简化函数参数的命名。
  • 命名要可读、可搜索。不要使用生僻的、不好读的英文单词来命名。除此之外,命名要符合项目的统一规范,不要用些反直觉的命名。
  • 接口有两种命名方式:一种是在接口中带前缀“I”;另一种是在接口的实现类中带后缀“Impl”。对于抽象类的命名,也有两种方式,一种是带上前缀“Abstract”,一种是不带前缀。这两种命名方式都可以,关键是要在项目中统一。

关于注释

  • 注释的目的就是让代码更容易看懂。只要符合这个要求的内容,你就可以将它写到注释里。总结一下,注释的内容主要包含这样三个方面:做什么、为什么、怎么做。对于一些复杂的类和接口,我们可能还需要写明“如何用”。
  • 注释本身有一定的维护成本,所以并非越多越好。类和函数一定要写注释,而且要写得尽可能全面、详细,而函数内部的注释要相对少一些,一般都是靠好的命名、提炼函数、解释性变量、总结性注释来提高代码可读性。

补充:

开发前,我一般先写注释,再写代码。比如写一个方法,我会先拆分业务逻辑,把注释给写上。后面再看注释,写代码。

// todo 
public void createOrder(RequestVo request) {
  // todo 校验用户登录
  // todo 校验商品
  // todo 创建订单
  // todo 拼装、返回结果集
}

关于注释:之前我的看法只要逻辑清晰命名准确达意就不用写注释了,现在回过来想这个问题,代码是需要不断维护的,即使当时你思路清晰那么过了一段时间后还能那么清晰么。人的大脑只会记住关键的信息,那么注释就是帮助我们梳理自己的想法和逻辑沉淀下来,是百利无害的事情,当别人接手也能迅速理解,降低沟通成本。如何注释才是好的注释呢?文中提到三点:做什么、为什么做、怎么做、怎么用(API)。这里最重要的事做什么,。我再补充一点,可以加下使用场景或者业务场景。

关于命名:这点我基本无疑义,总结下来就是两点:简洁达意和风格统一。

理论五:让你最快速地改善代码质量的20条编程规范(中)

1 函数、类多大才合适?

函数的代码行数不要超过一屏幕的大小,比如 50 行。类的大小限制比较难确定。

2.一行代码多长最合适?

最好不要超过 IDE 显示的宽度。当然,限制也不能太小,太小会导致很多稍微长点的语句被折成两行,也会影响到代码的整洁,不利于阅读。

3. 善用空行分割单元块

对于比较长的函数,为了让逻辑更加清晰,可以使用空行来分割各个代码块。在类内部,成员变量与函数之间、静态成员变量与普通成员变量之间、函数之间,甚至成员变量之间,都可以通过添加空行的方式,让不同模块的代码之间的界限更加明确。

4. 四格缩进还是两格缩进?

我个人比较推荐使用两格缩进,这样可以节省空间,特别是在代码嵌套层次比较深的情况下。除此之外,值得强调的是,不管是用两格缩进还是四格缩进,一定不要用 tab 键缩进。

5. 大括号是否要另起一行?

我个人还是比较推荐将大括号放到跟上一条语句同一行的风格,这样可以节省代码行数。但是,将大括号另起一行,也有它的优势,那就是,左右括号可以垂直对齐,哪些代码属于哪一个代码块,更加一目了然。

6. 类中成员的排列顺序

在 Google Java 编程规范中,依赖类按照字母序从小到大排列。类中先写成员变量后写函数。成员变量之间或函数之间,先写静态成员变量或函数,后写普通变量或函数,并且按照作用域大小依次排列。

关于编码技巧

1. 将复杂的逻辑提炼拆分成函数和类。

2. 通过拆分成多个函数或将参数封装为对象的方式,来处理参数过多的情况。

​ 我个人觉得,函数包含 3、4 个参数的时候还是能接受的,大于等于 5 个的时候,我们就觉得参数有点过多了,会影响到代码的可读性,使用起来也不方便。针对参数过多的情况,一般有 2 种处理方法。

  • 考虑函数是否职责单一,是否能通过拆分成多个函数的方式来减少参数。示例代码如下所示:

    public User getUser(String username, String telephone, String email);
    
    // 拆分成多个函数
    public User getUserByUsername(String username);
    public User getUserByTelephone(String telephone);
    public User getUserByEmail(String email);
    
  • 将函数的参数封装成对象。示例代码如下所示:

    
    public void postBlog(String title, String summary, String keywords, String content, String category, long authorId);
    
    // 将参数封装成对象
    public class Blog {
      private String title;
      private String summary;
      private String keywords;
      private Strint content;
      private String category;
      private long authorId;
    }
    public void postBlog(Blog blog);
    

3. 函数中不要使用参数来做代码执行逻辑的控制。

不要在函数中使用布尔类型的标识参数来控制内部逻辑,true 的时候走这块逻辑,false 的时候走另一块逻辑。这明显违背了单一职责原则和接口隔离原则。我建议将其拆成两个函数,可读性上也要更好。我举个例子来说明一下。

// 将其拆分成两个函数
public void buyCourse(long userId, long courseId);
public void buyCourseForVip(long userId, long courseId);

不过,如果函数是 private 私有函数,影响范围有限,或者拆分之后的两个函数经常同时被调用,我们可以酌情考虑保留标识参数。示例代码如下所示:


// 拆分成两个函数的调用方式
boolean isVip = false;
//...省略其他逻辑...
if (isVip) {
  buyCourseForVip(userId, courseId);
} else {
  buyCourse(userId, courseId);
}

// 保留标识参数的调用方式更加简洁
boolean isVip = false;
//...省略其他逻辑...
buyCourse(userId, courseId, isVip);

除了布尔类型作为标识参数来控制逻辑的情况外,还有一种“根据参数是否为 null”来控制逻辑的情况。针对这种情况,我们也应该将其拆分成多个函数。拆分之后的函数职责更明确,不容易用错。具体代码示例如下所示:


public List<Transaction> selectTransactions(Long userId, Date startDate, Date endDate) {
  if (startDate != null && endDate != null) {
    // 查询两个时间区间的transactions
  }
  if (startDate != null && endDate == null) {
    // 查询startDate之后的所有transactions
  }
  if (startDate == null && endDate != null) {
    // 查询endDate之前的所有transactions
  }
  if (startDate == null && endDate == null) {
    // 查询所有的transactions
  }
}

// 拆分成多个public函数,更加清晰、易用
public List<Transaction> selectTransactionsBetween(Long userId, Date startDate, Date endDate) {
  return selectTransactions(userId, startDate, endDate);
}

public List<Transaction> selectTransactionsStartWith(Long userId, Date startDate) {
  return selectTransactions(userId, startDate, null);
}

public List<Transaction> selectTransactionsEndWith(Long userId, Date endDate) {
  return selectTransactions(userId, null, endDate);
}

public List<Transaction> selectAllTransactions(Long userId) {
  return selectTransactions(userId, null, null);
}

private List<Transaction> selectTransactions(Long userId, Date startDate, Date endDate) {
  // ...
}

4. 函数设计要职责单一。

我们在前面讲到单一职责原则的时候,针对的是类、模块这样的应用对象。实际上,对于函数的设计来说,更要满足单一职责原则。相对于类和模块,函数的粒度比较小,代码行数少,所以在应用单一职责原则的时候,没有像应用到类或者模块那样模棱两可,能多单一就多单一。

具体的代码示例如下所示:


public boolean checkUserIfExisting(String telephone, String username, String email)  { 
  if (!StringUtils.isBlank(telephone)) {
    User user = userRepo.selectUserByTelephone(telephone);
    return user != null;
  }
  
  if (!StringUtils.isBlank(username)) {
    User user = userRepo.selectUserByUsername(username);
    return user != null;
  }
  
  if (!StringUtils.isBlank(email)) {
    User user = userRepo.selectUserByEmail(email);
    return user != null;
  }
  
  return false;
}

// 拆分成三个函数
public boolean checkUserIfExistingByTelephone(String telephone);
public boolean checkUserIfExistingByUsername(String username);
public boolean checkUserIfExistingByEmail(String email);

5. 移除过深的嵌套层次,方法包括:去掉多余的 if 或 else 语句,使用 continue、break、return 关键字提前退出嵌套,调整执行顺序来减少嵌套,将部分嵌套逻辑抽象成函数。

代码嵌套层次过深往往是因为 if-else、switch-case、for 循环过度嵌套导致的。我个人建议,嵌套最好不超过两层,超过两层之后就要思考一下是否可以减少嵌套。过深的嵌套本身理解起来就比较费劲,除此之外,嵌套过深很容易因为代码多次缩进,导致嵌套内部的语句超过一行的长度而折成两行,影响代码的整洁。解决嵌套过深的方法也比较成熟,有下面 4 种常见的思路。

  • 去掉多余的 if 或 else 语句。代码示例如下所示:

    
    // 示例一
    public double caculateTotalAmount(List<Order> orders) {
      if (orders == null || orders.isEmpty()) {
        return 0.0;
      } else { // 此处的else可以去掉
        double amount = 0.0;
        for (Order order : orders) {
          if (order != null) {
            amount += (order.getCount() * order.getPrice());
          }
        }
        return amount;
      }
    }
    
    // 示例二
    public List<String> matchStrings(List<String> strList,String substr) {
      List<String> matchedStrings = new ArrayList<>();
      if (strList != null && substr != null) {
        for (String str : strList) {
          if (str != null) { // 跟下面的if语句可以合并在一起
            if (str.contains(substr)) {
              matchedStrings.add(str);
            }
          }
        }
      }
      return matchedStrings;
    }
    
  • 调整执行顺序来减少嵌套。具体的代码示例如下所示:

    
    // 重构前的代码
    public List<String> matchStrings(List<String> strList,String substr) {
      List<String> matchedStrings = new ArrayList<>();
      if (strList != null && substr != null) {
        for (String str : strList) {
          if (str != null) {
            if (str.contains(substr)) {
              matchedStrings.add(str);
            }
          }
        }
      }
      return matchedStrings;
    }
    
    // 重构后的代码:先执行判空逻辑,再执行正常逻辑
    public List<String> matchStrings(List<String> strList,String substr) {
      if (strList == null || substr == null) { //先判空
        return Collections.emptyList();
      }
    
      List<String> matchedStrings = new ArrayList<>();
      for (String str : strList) {
        if (str != null) {
          if (str.contains(substr)) {
            matchedStrings.add(str);
          }
        }
      }
      return matchedStrings;
    }
    
  • 将部分嵌套逻辑封装成函数调用,以此来减少嵌套。具体的代码示例如下所示:

    
    // 重构前的代码
    public List<String> appendSalts(List<String> passwords) {
      if (passwords == null || passwords.isEmpty()) {
        return Collections.emptyList();
      }
      
      List<String> passwordsWithSalt = new ArrayList<>();
      for (String password : passwords) {
        if (password == null) {
          continue;
        }
        if (password.length() < 8) {
          // ...
        } else {
          // ...
        }
      }
      return passwordsWithSalt;
    }
    
    // 重构后的代码:将部分逻辑抽成函数
    public List<String> appendSalts(List<String> passwords) {
      if (passwords == null || passwords.isEmpty()) {
        return Collections.emptyList();
      }
    
      List<String> passwordsWithSalt = new ArrayList<>();
      for (String password : passwords) {
        if (password == null) {
          continue;
        }
        passwordsWithSalt.add(appendSalt(password));
      }
      return passwordsWithSalt;
    }
    
    private String appendSalt(String password) {
      String passwordWithSalt = password;
      if (password.length() < 8) {
        // ...
      } else {
        // ...
      }
      return passwordWithSalt;
    }
    

    除此之外,常用的还有通过使用多态来替代 if-else、switch-case 条件判断的方法。这个思路涉及代码结构的改动。

6. 用字面常量取代魔法数。

常用的用解释性变量来提高代码的可读性的情况有下面 2 种.

  • 常量取代魔法数字。示例代码如下所示:

    
    public double CalculateCircularArea(double radius) {
      return (3.1415) * radius * radius;
    }
    
    // 常量替代魔法数字
    public static final Double PI = 3.1415;
    public double CalculateCircularArea(double radius) {
      return PI * radius * radius;
    }
    
  • 使用解释性变量来解释复杂表达式。示例代码如下所示:

    
    if (date.after(SUMMER_START) && date.before(SUMMER_END)) {
      // ...
    } else {
      // ...
    }
    
    // 引入解释性变量后逻辑更加清晰
    boolean isSummer = date.after(SUMMER_START)&&date.before(SUMMER_END);
    if (isSummer) {
      // ...
    } else {
      // ...
    } 
    

7. 用解释性变量来解释复杂表达式,以此提高代码可读性。

https://time.geekbang.org/column/article/188882

其他《阿里 JAVA 规范》

OOP 规约:

1. 【强制】POJO 类中的任何布尔类型的变量,都不要加 is 前缀,否则部分框架解析会引起序列化错误。

说明:在本文 mysql 规约中的建表约定第一条,表达是与否的变量采用 is_xxx 的命名方式,所以,需要

在设置从 is_xxx 到 xxx 的映射关系。

反例:定义为基本数据类型 Boolean isDeleted 的属性,它的方法也是 isDeleted(),框架在反向解析的时

候,“误以为”对应的属性名称是 deleted,导致属性获取不到,进而抛出异常。

2. 【推荐】在常量与变量的命名时,表示类型的名词放在词尾,以提升辨识度。

正例:startTime / workQueue / nameList / TERMINATED_THREAD_COUNT

反例:startedAt / QueueOfWork / listName / COUNT_TERMINATED_THREAD

3. 【强制】注释的双斜线与注释内容之间有且仅有一个空格。

正例:
// 这是示例注释,请注意在双斜线之后有一个空格
String commentString = new String();

4.【强制】 POJO 类必须写 toString 方法。

使用 IDE 中的工具:source> generate toString时,如果继承了另一个 POJO 类,注意在前面加一下 super.toString。

说明:在方法执行抛出异常时,可以直接调用 POJO 的 toString()方法打印其属性值,便于排查问题。

5. 【强制】构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在 init 方法中。

6. 【强制】定义 DO/DTO/VO 等 POJO 类时,不要设定任何属性默认值。

反例:POJO 类的 createTime 默认值为 new Date(),但是这个属性在数据提取时并没有置入具体值,在更新其它字段时又附带更新了此字段,导致创建时间被修改成当前时间。

7. 【推荐】final 可以声明类、成员变量、方法、以及本地变量,下列情况使用 final 关键字:

1) 不允许被继承的类,如:String 类。

2) 不允许修改引用的域对象,如:POJO 类的域变量。

3) 不允许被覆写的方法,如:POJO 类的 setter 方法。

4) 不允许运行过程中重新赋值的局部变量。

5) 避免上下文重复使用一个变量,使用 final 关键字可以强制重新定义一个变量,方便更好地进行重构。

8. 【推荐】类成员与方法访问控制从严:

1) 如果不允许外部直接通过 new 来创建对象,那么构造方法必须是 private。

2) 工具类不允许有 public 或 default 构造方法。

3) 类非 static 成员变量并且与子类共享,必须是 protected。

4) 类非 static 成员变量并且仅在本类使用,必须是 private。

5) 类 static 成员变量如果仅在本类使用,必须是 private。

6) 若是 static 成员变量,考虑是否为 final。

7) 类成员方法只供类内部调用,必须是 private。

8) 类成员方法只对继承类公开,那么限制为 protected。

说明:任何类、方法、参数、变量,严控访问范围。过于宽泛的访问范围,不利于模块解耦。

思考:如果是一个 private 的方法,想删除就删除,可是一个 public 的 service 成员方法或成员变量,删除一下,不得手心冒点汗吗?变量像自己的小孩,尽量在自己的视线内,变量作用域太大,无限制的到处跑,那么你会担心的。

日期时间:

1. 【强制】日期格式化时,传入 pattern 中表示年份统一使用小写的 y。

说明:日期格式化时,yyyy 表示当天所在的年,而大写的 YYYY 代表是 week in which year(JDK7 之后引入的概念),意思是当天所在的周属于的年份,一周从周日开始,周六结束,只要本周跨年,返回的 YYYY 就是下一年。

正例:表示日期和时间的格式如下所示:

new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")

2. 【强制】在日期格式中分清楚大写的 M 和小写的 m,大写的 H 和小写的 h 分别指代的意义。

说明:日期格式中的这两对字母表意如下:

1) 表示月份是大写的 M;

2) 表示分钟则是小写的 m;

3) 24 小时制的是大写的 H;

4) 12 小时制的则是小写的 h。

3. 【推荐】使用枚举值来指代月份。如果使用数字,注意 Date,Calendar 等日期相关类的月份 month 取值在 0-11 之间。

说明:参考 JDK 原生注释,Month value is 0-based. e.g., 0 for January.

正例: Calendar.JANUARY,Calendar.FEBRUARY,Calendar.MARCH 等来指代相应月份来进行传参或比较。

集合处理:

1.【强制】关于 hashCode 和 equals 的处理,遵循如下规则:

1) 只要覆写 equals,就必须覆写 hashCode。

2) 因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须覆写这两种方法。

3) 如果自定义对象作为 Map 的键,那么必须覆写 hashCode 和 equals。

说明:String 因为覆写了 hashCode 和 equals 方法,所以可以愉快地将 String 对象作为 key 来使用。

2.【强制】判断所有集合内部的元素是否为空,使用 isEmpty()方法,而不是 size()==0 的方式。

说明:在某些集合中,前者的时间复杂度为 O(1),而且可读性更好。

正例:

Map<String, Object> map = new HashMap<>(16);

if(map.isEmpty()) {
		System.out.println("no element in this map.");
}

3.【强制】在使用 Collection 接口任何实现类的 addAll()方法时,都要对输入的集合参数进行NPE 判断。

说明:在 ArrayList#addAll 方法的第一行代码即 Object[] a = c.toArray(); 其中 c 为输入集合参数,如果为 null,则直接抛出异常。

4.【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator方式,如果并发操作,需要对 Iterator 对象加锁。

正例:

List<String> list = new ArrayList<>();

list.add("1");

list.add("2");

Iterator<String> iterator = list.iterator();

while (iterator.hasNext让你最快速地改善代码质量的 20 条编程规范

改善代码质量编程规范 - 学习总结

改善代码质量编程规范 - 学习总结

编写高质量代码改善C#程序的157个建议——建议134:有条件地使用前缀

手把手使用SonarQube分析改善项目代码质量

敏捷项目中代码质量提升实践