快速改善代码质量的20条编程规范

快速改善代码质量的20条编程规范

主要分为三大块:

命名和注释 (Naming and Comments)
命名(Naming)
  1. 命名多长最合适?

    命名长度有两种极端:1. 单词全名命名法 2. 熟知单词缩写命名法

    1. 优点:更加准确直观达意

      缺点:在代码长度有限制的情况下,就会经常出现一条语句被分割成两行的情况,这其实会影响代码可读性

    2. 优点:简洁明了

      缺点:不够达意

    对于作用域比较小的,推荐使用相对短的命名,比如一些函数内的临时变量。

    对于像类名这种作用域比较大的,推荐使用长的命名方式。

    命名的时候,开发人员应当学会换位思考,假设自己不熟悉这块代码,从代码阅读者的角度去考量命名是否足够直观

  2. 利用上下文简化命名

    类名和函数名是一种上下文信息

    对于像一个类中的属性命名,开发人员可以在该类对象的帮助下可以准确明白该属性表达的含义。

    对于函数参数的命名而言,开发人员可以借助函数名的来命名参数。

  3. 命名要可读、要可搜索

    可读:指的是不要用一些特别生僻、难发音的英文单词来命名

    可搜索:开发人员在IDE中编写代码的时候,经常会用“关键词联想”的方法来自动补全和搜索。开发人员在命名的时候,最好能符合整个项目的命名习惯。

    统一规约是很重要的,能减少很多不必要的麻烦。

  4. 如何命名接口和抽象类?

    接口命名的两种方式:1. 接口名加上前缀“I” 2. 接口名不加前缀“I”,对应的实现类加上后缀“Impl”

    抽象类的两种命名方式 :1. 抽象类名加上前缀“Abstract” 2. 抽象类名不加前缀“Abstract”

    对于接口和抽象类的命名无论哪种命名方式均可,只要项目里能够统一即可。

注释(Comments)
  1. 注释到底该写什么?

    注释的内容主要包含这样三个方面:

    1. 做什么?
    2. 为什么?
    3. 怎么做?
    /**
    * (what) Bean factory to create beans. 
    * 
    * (why) The class likes Spring IOC framework, but is more lightweight. 
    *
    * (how) Create objects from different sources sequentially:
    * user specified object > SPI > configuration > default object.
    */
    public class BeansFactory {
      // ...
    }
    

    这样写注释的主要观点如下:

    • 注释比代码承载的信息更多:类名的命名不够详尽,需在注释中写明“做什么”,因为类中包含的信息较多。函数命名则不一样,一个好的命名却可以准确表达函数的功能,这时可以不需要在注释中解释它是做什么的。
    • 注释起到总结性作用、文档的作用:在注释中,关于具体的代码实现思路,我们可以写一些总结性的说明、特殊情况的说明。这样能够让阅读代码的人通过注释就能大概了解代码的实现思路,阅读起来就会更加容易。实际上,对于有些比较复杂的类或者接口,我们可能还需要在注释中写清楚“如何用”,举一些简单的 quick start 的例子,让使用者在不阅读代码的情况下,快速地知道该如何使用。
    • 一些总结性注释能让代码结构更清晰:对于逻辑比较复杂的代码或者比较长的函数,如果不好提炼、不好拆分成小的函数调用,那我们可以借助总结性的注释来让代码结构更清晰、更有条理。
  2. 注释是不是越多越好?

    注释过多和过少都有一定的问题。注释过多代表代码不够可读,并且的过多的注释也会一定程度上影响代码可读性。维护成本较高,代码的改动忘记同步修改注释会造成代码阅读者的迷惑。

    按照我的经验来说,类和函数一定要写注释,而且要写得尽可能全面、详细,而函数内部的注释要相对少一些,一般都是靠好的命名、提炼函数、解释性变量、总结性注释来提高代码的可读性。

代码风格(Code Style)
  1. 函数、类多大才合适?

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

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

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

  3. 善用空行分割单元块

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

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

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

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

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

  6. 类中成员排列顺序

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

编程技巧(Coding Tips)
  1. 把代码分割成更小的单元块

    开发人员需要具有模块化和抽象思维,善于将大块的复杂逻辑提炼成类或者函数,屏蔽掉细节,让阅读代码的人不至于迷失在细节中,这样能极大的提高代码的可读性。只有代码逻辑比较复杂的时候,我们其实才建议提炼成类或者函数;反之,如果提炼出的函数只包含两三行代码,在阅读代码的时候,还得跳来跳去的阅读代码,这样反倒增加了阅读成本。

  2. 避免函数参数过多

    函数包含3、4个参数的时候还是能接受的,大于等于5个的时候,这样参数的个数就有点过多了,会影响到代码的可读性,使用起来也不方便。

    针对参数过多的情况,分为以下两种处理方法:

    • 考虑函数是否职责单一,是否能通过拆分成多个函数的方式来减少参数。

      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, boolean isVip);
    
    // 将其拆分成两个函数
    public void buyCourse(long userId, long courseId);
    public void buyCourseForVip(long userId, long courseId);
    

    除了布尔类型作为标识参数来控制逻辑的情况外,还有一种“根据参数是否为 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、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;
      }
      
    • **使用编程语言提供的 continue、break、return 关键字,提前退出嵌套。**代码示例如下所示:

      
      // 重构前的代码
      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 && str.contains(substr)) {
              matchedStrings.add(str);
              // 此处还有10行代码...
            }
          }
        }
        return matchedStrings;
      }
      
      // 重构后的代码:使用continue提前退出
      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 || !str.contains(substr)) {
              continue; 
            }
            matchedStrings.add(str);
            // 此处还有10行代码...
          }
        }
        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;
      }
      
  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 {
        // ...
      } 
      
统一编码规范 (Uniform Coding Standards)

那就是,项目、团队,甚至公司,一定要制定统一的编码规范,并且通过 Code Review 督促执行,这对提高代码质量有立竿见影的效果。

说明:本文内容摘抄自极客时间《设计模式之美》专栏

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值