10k

设计模式之美-课程笔记21-编程规范20则

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

命名

  1. 命名多长合适:在能够表达含义的情况下,命名越短越好。但是大部分情况短名字又不能很好表达意思。对于熟知的词可以采用缩写例如second缩写为sec,string缩写为str,number为num。还有一些临时变量或者作用域比较小的变量。反正则需要长一些,考虑到可能被看到地方较多,要让别人看懂。
  2. 利用上下文简化命名
public class User {
  private String userName;
  private String userPassword;
  private String userAvatarUrl;
  //...
}

在这里我们可以简化变量的命名,因为在调用的时候可以借助对象+属性的名称来明确表达意思:

User user = new User();
user.getName(); // 借助user对象这个上下文

另外也函数参数也可以借助函数名上下文来简化命名:

public void uploadUserAvatarImageToAliyun(String userAvatarImageUri);
//利用上下文简化为:
public void uploadUserAvatarImageToAliyun(String imageUri);
  1. 命名要可读可搜索

    • 可读:避免使用生僻词汇。
    • 可搜索:get相关的方法都用get,避免自立规范,又去用query或者list之类的方法来表达同样的意思,实现同类型的函数。 大家都是selectXXX,自己就避免使用queryXXX;大家是addXXX你就别用insertXXX
  2. 如何命名接口和抽象类

    • 接口: 一种事以字母"I"开头(取interface首字符),例如IUserSerbice,实现类是UserService;另一种不加前缀,但是实现类加Impl在后面,接口是UserService,实现类是UserServiceImpl
    • 抽象类:也有两种方式,一种是带上前缀“Abstract”,比如 AbstractConfiguration;另一种是不带前缀“Abstract”。
    • 反正还是项目中要整体规范统一即可。

注释

  1. 注释到底该写什么:做什么、为什么、怎么做。

    • 注释比代码的信息更多。有时候函数名可以反映函数做什么,但是有时候一个类的东西比较多,就不太好通过名字判断。在注释中写出做什么就有意义。
    • 注释起到总结、文档作用。简要介绍实现思路、特殊情况能够让代码更易读。甚至对应复杂接口还可以添加一些简单的例子。
    • 让代码结构更清晰。

    ```java public boolean isValidPasword(String password) { // check if password is null or empty if (StringUtils.isBlank(password)) { return false; }

    // check if the length of password is between 4 and 64 int length = password.length(); if (length < 4 || length > 64) { return false; }

    // check if password contains only a~z,0~9,dot for (int i = 0; i < length; ++i) { char c = password.charAt(i); if (!((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.')) { return false; } } return true; } ```

  2. 注释愈多越好?:凡事有个度。太少可能介绍不明白,太多可能又意味着代码本身可读性差,而且过多注释又影响代码可读性。如果代码一改忘记改注释还会造成疑惑。

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

代码风格

  1. 类、函数多大才合适:就像炒菜说盐少许,这个少许就很难量化。

    • 一个有意思的说法,函数长度不要超过显示屏的垂直高度。在IDE中不需要滚来滚去前后切换。
    • 对于类的长度就更难量化,在你感觉一个类让你使用起来头大,找不到函数;或者每次都有引用一整个类即使不需要,那么说明他有点大了。
  2. 一行代码最长多长合适?Google Java Style Code 建议是100个字符,但是实际情况团队自定,一般不超过一个显示屏宽度最好。但是也不能太小导致长语句被折成两行。

  3. 善于用空行分割单元块: 在不方便将独立的逻辑代码块拆分成多个函数的情况下为了让逻辑清晰,不仅可以用注释,还可以用空行分割。另外,在成员变量和函数之间、静态成员与普通成员变量之间、函数之间甚至是各个成员变量之间,都可以利用空行,让代码更有结构性和清晰有条理。

  4. 四格缩进还是两格缩进?: 内部统一,个人喜好。别用tab,因为tab在不同的ide下有的是四格有的是两格有的是空格缩进有的使用一个tab缩进都会造成混乱。

  5. 大括号另起一行?:个人喜好,团队统一。

  6. 类中成员的排列顺序。

    1. Google编码规范中,依赖类按照字母顺序排序;
    2. 类中成员变量在函数前, 成员变量或者函数之间,先静态、后普通。
    3. 成员变量或者函数之间,按照作用域从大到小,先public,在protected再private。

编程技巧

  1. 把代码分割成更小的单元块:因为人倾向于先看整体再看细节,所以将代码片段提炼成函数,屏蔽细节,可以提升代码的可读性。
// 重构前的代码
public void invest(long userId, long financialProductId) {
  Calendar calendar = Calendar.getInstance();
  calendar.setTime(date);
  calendar.set(Calendar.DATE, (calendar.get(Calendar.DATE) + 1));
  if (calendar.get(Calendar.DAY_OF_MONTH) == 1) {
    return;
  }
  //...
}

// 重构后的代码:提炼函数之后逻辑更加清晰
public void invest(long userId, long financialProductId) {
  if (isLastDayOfMonth(new Date())) {
    return;
  }
  //...
}

public boolean isLastDayOfMonth(Date date) {
  Calendar calendar = Calendar.getInstance();
  calendar.setTime(date);
  calendar.set(Calendar.DATE, (calendar.get(Calendar.DATE) + 1));
  if (calendar.get(Calendar.DAY_OF_MONTH) == 1) {
   return true;
  }
  return false;
}

这个也可以靠到SRP上,单一责任原则,invest函数中尽量不包含一个日期处理的实现。

  1. 避免函数参数过多: 也是可能会影响可读性。
    1. 考虑职责是否单一,能否拆分成多个函数的方式来减少参数。例如:
public User getUser(String username, String telephone, String email);

// 拆分成多个函数
public User getUserByUsername(String username);
public User getUserByTelephone(String telephone);
public User getUserByEmail(String email);

​ 2. 将函数的参数封装成对象,例如

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);

​ 这样做还能提高接口的兼容性。如果这个函数是对外暴露的远程接口,当修改参数的时候,只需要该对象就好了,远程调用可能就不需要修改代码来兼容。

  1. 不用函数参数来控制逻辑

不要在函数中使用布尔类型的标识参数来控制内部逻辑,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);

不过如果函数是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) {
  // ...
}
  1. 函数设计职责要单一: 尽量单一,小粒度。
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);
  1. 移除过沈的嵌套层次: 一般由if-else或者switch-case或for循环引起,最好别超过两层,超过了可读性也不好,又深,又可能引起换行。
    • 去掉多余的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;
}
  1. 学会使用解释性变量
    • 常量代替魔法数字
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 {
  // ...
} 
Thoughts? Leave a comment