代码可读性之道

代码可读性之道

1 代码可读性

程序员常常调侃自己,失业了就去送外卖、搬砖,虽然是个玩笑话,但是也直击了问题的本质。在软件开发的工地上,写代码的程序员真的等于搬砖工人。

职能 建筑工程 软件工程
设计 建筑设计师 架构师、团队负责人
管理 建造工程师、造价工程师、项目经理、施工员 团队负责人
施工 架子工、抹灰工、砌筑工、混凝土工等 程序员
质量 监理工程师 软件测试
后勤 食堂、宿舍 系统运维

建筑行业的标准非常多,比如房屋建筑制图标准、硅酸盐水泥国家标准、建筑用钢筋国家标准等等。如果不遵守这些标准,工地无法展开工作或者建筑质量低下。与建筑工程不同是,软件是智力产品,可以反复迭代升级。

代码也是团队的沟通方式之一。试想,其他同事接手你负责的功能,如果代码结构清晰、注释合理,他就不会频繁打断你的工作、询问代码中的疑点。编写代码的时候,首先要考虑到别人的阅读感受,而不是你自己。在实际的开发工作中,最花费时间的事情是理解当前代码以及上下游代码逻辑。如果需要变更代码,代码可读性越差,花的时间越长,这个模块的可维护性就越低。

代码的可读性到底怎么定义呢?维护者很容易看出代码的意图,代码的真实逻辑按照维护者的直观感受来执行

2 合适的命名

起名字是头等大事,金庸绝对是起名的高手。《天龙八部》里有“南慕容北乔峰”,以乔峰为例,这个“峰”字听上去伟岸、有英雄气,如果叫乔发、乔雄、乔布斯,就像个暴发户,不像大英雄。再说慕容复,复姓“慕容”非常文雅,长得也是翩翩公子,为了“复”国走上歧路,最后众叛亲离。

好的命名能够自己表达意图,降低阅读者的心智负担。编程语言都是以英文为基础,但是要想起好名字,不光要英语好,还要深刻的理解业务逻辑,才能做到“信、达、雅”。“信”指的是准确,不能偏离,也不可以遗漏;“达”指的是不拘泥细节,用词通顺清晰;“雅”指的是选用的词语要得体。

2.1 确定领域词汇

软件系统用来支撑特定领域的业务,每个领域都有专业词汇。在项目开发初期,通过宣讲或者文档的方式确定业务领域词汇,有助于提高代码可读性。以电商领域为例,通常会出现下面的词汇:

英文 全称 解释
order order 订单
product product 商品
sku Stock Keeping Unit 最小存货单位
payment payment 支付

遇到不确定的词,不要闭门造车,要抛出来集体讨论确认,比如互金领域有个“分期”的业务,对应的英文有“stage”、“installment”。如果集体决定用“installment”,就固定下来,不准再使用“stage”。

2.2 遵守约定俗成

任何编程语言都有俗成的命名约定或者单词缩写,比如循环计数器可以被命名为i、j或k,以Java语言为例:

  • 驼峰命名:名称由多个单词构成,从第二个单词开始,所有的单词首字母必须大写,如:firstName、ActionEvent、ActionListener
  • 类名称:以大写字母开头,包含名词,如: Color、Button、System、Thread
  • 接口名称:以大写字母开头,包含形容词,如: Runnable、Remote、ActionListener
  • 方法名称:以小写字母开头,包含动词,如:actionPerformed()、main()、print()
  • 常量名称: 全部大写字母,如RED、YELLOW、MAX_PRIORITY
  • 常用缩写:document 缩写 doc ; string 缩写 str ;text 缩写 txt

2.3 附带额外信息

附加一些额外信息可以提升命名的精度,通常从单位、属性、格式三个方面考虑,但是要注意长度。如下代码所示:

int CACHE_EXPIRE = 60;
// 增加了second 表示单位是秒
int CACHE_EXPIRE_SECOND = 60;

String content = "MTIz";
// 增加了base64 表示编码方式是base64
String base64Content = "MTIz";

int totalMemory = 1024;
// 增加了 MB 表示容量单位是兆
int totalMemoryMB = 1024;

2.4 区分方法参数

如果一个方法有多个含义相同的参数,要加一些形容词区分参数的用途,如下代码所示:

private void modifyPassword(String password1 ,String password2)

以上代码示例,方法使用者难以区分两个password的用法,改写后:

private void modifyPassword(String oldPassowrd,String newPassword)

3 短小的方法

方法要多短小,才最合适呢? 这个没有固定结论,一般不要超过半个屏幕。如果一个方法几个屏幕滚不完,阅读者看了后面忘记前面,绝对要骂人。一定要把长方法拆分成功能内聚的短方法,如下代码所示:

//  获取个人信息
private UserDTO getUserDTO(Integer userId)
{
    //获取基本信息
    … 此处写了10行

    //获取最近的一次订单信息
    … 此处写了30行

   // 获取钱包余额、可用优惠券张数等
    … 此处写了30行

   return userDTO;
}

改写如下:

//  获取个人信息
private UserDTO getUserDTO(Integer userId)
{
    //获取基本信息 
    UserDTO userDTO= getUserBasicInfo(userId);

    //获取最近的一次订单信息
    userDTO.setUserLastOrder(getUserLastOrder(userId));

    // 获取钱包、可用优惠券张数等
    userDTO.setUserAccount(getUserAccount(userId));  
    return userDTO;
}

private UserDTO getUserBasicInfo(Integer userId);
private UserLastOrder getUserLastOrder(Integer userId);
private UserAccount getUserAccount(Integer userId);

4 减少嵌套

4.1 减少if/else嵌套

为什么要减少嵌套,难道嵌套不时尚吗?我曾经看到一个同事的代码嵌套达到9层,他自己维护的时候都看晕了。如下代码所示:

// 修改用户密码,这个例子只有3层嵌套,很温柔了
public boolean modifyPassword(Integer userId, String oldPassword, String newPassword) {
      if (userId != null && StringUtils.isNotBlank(newPassword) && 
          SpringUtils.isNotBlank(oldPassword)) {
          User user = getUserById(userId);

          if (user != null) {
            if (user.getPassword().equals(oldPassword) {
               return updatePassword(userId, newPassword)
            }
          }
      }
}

改写如下:

// 修改用户密码 
Public Boolean modifyPassword(Integer userId, String oldPassword, String newPassword) {
     if (userId == null || StringUtils.isBlank(newPassword) || 
        StringUtils.isBlank(oldPassword)) {
            return false;
     }
     User user = getUserById(userId);
     if(user == null) {
           return false;
      }
     if(!user.getPassword().equals(oldPassword) {
           return false;    
     }
     return updatePassword(userId, newPassword);
}

采用卫语句减少了嵌套,但是并非所有场景都适合这样改写,也可以将关联性强的逻辑抽取成一个独立的方法减少嵌套。

4.2 抽离try/catch

大家有没有见过一个超长的方法,从头到尾被一个try/catch照顾着?我经历过的项目中,这种不负责的写法比比皆是。并非每行代码都会抛出错误,只要将会抛出错误的业务放在一个独立的方法即可。如下代码所示:

//  获取个人信息
private UserDTO getUserDTO(Integer userId)
{
   try { 
       //获取基本信息 
       ... 此处写了10行
       //获取最近的一次订单信息.
       ...此处写了20行
       // 获取钱包、可用优惠券张数等
       ...此处写了20行
    }catch (Exception e) {
        logger.error(e);
        return null;
    }
}
   return userDTO;
}

改写如下:

//  获取个人信息
private UserDTO getUserDTO(Integer userId)
{
    //获取基本信息 
    UserDTO userDTO= getUserBasicInfo(userId);

    //获取最近的一次订单信息
    userDTO.setUserLastOrder(getUserLastOrder(userId));

    // 获取钱包、可用优惠券张数等
    userDTO.setUserAccount(getUserAccount(userId));  
    return userDTO;
}
private  UserDTO getUserBasicInfo(Integer userId);
private  UserLastOrder getUserLastOrder(Integer userId);
private  UserAccount getUserAccount(Integer userId){
      try{
          // TODO
      } catch ( Exception e) 
       { //TODO }
}

5 封装参数

如果方法参数将超过3个,建议放在类中包装起来,否则再增加参数时,由于语义的强耦合会导致调用方语法错误。在后台管理中的分页查询接口,常常会有很多查询参数,而且有可能增加,封装起来是最好的。

// 分页查询订单 6个参数
public Page<Order> queryOrderByPage(Integer current,Integer size,String productName,Integer userId,Date startTime,Date endTime,Bigdecimal minAmount ,Bigdecimal maxAmount) {

}

改写如下:

public class OrderQueryDTO extends PageDTO {
 private String productName;
 private Integer userId;
 private Date startTime;
 private Date endTime;
 private Bigdecimal minAmount ;
 private Bigdecimal maxAmount;
}

// 分页查询订单
Public Page<Order> queryOrderByPage(OrderQueryDTO orderQueryDTO) {

}

6 注释规范

注释的使用原则是:如果代码能够说清楚,就不要注释。许多程序员犯的错误,不是注释少了,而是多了,主要体现在以下几个方面:

  • 多余的注释:注释没有提供比代码更多的信息,也没有解释代码的意图或逻辑,读代码比读注释还要容易,这种注释就是多余的。
  • 误导性注释:代码随着需求变动,注释也要随之变动,不准确的注释不如没注释。
  • 日志式注释:每次编辑代码都在模块处理添加一条注释,例如:2018-10-02增加了某某功能。通过源代码控制系统的 commit 字段就可以保存这种信息,这一类注释是画蛇添足。
  • 署名注释:随着不断迭代,代码将会与原作者脱离关系,署名注释没有价值。
  • 注释掉的代码:注释掉的代码让读者产生疑问,这个代码到底有用还是没用?如果有用,为什么要注释掉,如果没用,为什么不删除?

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注