IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    设计模式之美-课程笔记23-编程规范&代码质量实战(ID生成器)2 - 异常处理

    10k发表于 2023-07-23 00:00:00
    love 0

    程序出错该返回啥?NULL、异常、错误码、空对象?

    public class RandomIdGenerator implements IdGenerator {
      private static final Logger logger = LoggerFactory.getLogger(RandomIdGenerator.class);
    
      @Override
      public String generate() {
        String substrOfHostName = getLastFiledOfHostName();
        long currentTimeMillis = System.currentTimeMillis();
        String randomString = generateRandomAlphameric(8);
        String id = String.format("%s-%d-%s",
                substrOfHostName, currentTimeMillis, randomString);
        return id;
      }
    
      private String getLastFiledOfHostName() {
        String substrOfHostName = null;
        try {
          String hostName = InetAddress.getLocalHost().getHostName();
          substrOfHostName = getLastSubstrSplittedByDot(hostName);
        } catch (UnknownHostException e) {
          logger.warn("Failed to get the host name.", e);
        }
        return substrOfHostName;
      }
    
      @VisibleForTesting
      protected String getLastSubstrSplittedByDot(String hostName) {
        String[] tokens = hostName.split("\\.");
        String substrOfHostName = tokens[tokens.length - 1];
        return substrOfHostName;
      }
    
      @VisibleForTesting
      protected String generateRandomAlphameric(int length) {
        char[] randomChars = new char[length];
        int count = 0;
        Random random = new Random();
        while (count < length) {
          int maxAscii = 'z';
          int randomAscii = random.nextInt(maxAscii);
          boolean isDigit= randomAscii >= '0' && randomAscii <= '9';
          boolean isUppercase= randomAscii >= 'A' && randomAscii <= 'Z';
          boolean isLowercase= randomAscii >= 'a' && randomAscii <= 'z';
          if (isDigit|| isUppercase || isLowercase) {
            randomChars[count] = (char) (randomAscii);
            ++count;
          }
        }
        return new String(randomChars);
      }
    }
    
    • 对于 generate() 函数,如果本机名获取失败,函数返回什么?这样的返回值是否合理?
    • 对于 getLastFiledOfHostName() 函数,是否应该将 UnknownHostException 异常在函数内部吞掉(try-catch 并打印日志)?还是应该将异常继续往上抛出?如果往上抛出的话,是直接把 UnknownHostException 异常原封不动地抛出,还是封装成新的异常抛出?
    • 对于 getLastSubstrSplittedByDot(String hostName) 函数,如果 hostName 为 NULL 或者是空字符串,这个函数应该返回什么?
    • 对于 generateRandomAlphameric(int length) 函数,如果 length 小于 0 或者等于 0,这个函数应该返回什么?

    函数出错返回什么?

    函数出错一般有四种情况:异常对象、空、null、错误码

    1. 返回错误码

    C语言中没有异常处理机制,所以一般用错误码处理异常。Java个Python因为有异常处理机制所以不用错误码。

    C中错误码返回有两种:一种是直接作为函数返回返回,另一种是错误码定义为全局变量,执行出错时函数调用者用这个全局变量获取错误码。

    // 错误码的返回方式一:pathname/flags/mode为入参;fd为出参,存储打开的文件句柄。
    int open(const char *pathname, int flags, mode_t mode, int* fd) {
      if (/*文件不存在*/) {
        return EEXIST;
      }
      
      if (/*没有访问权限*/) {
        return EACCESS;
      }
      
      if (/*打开文件成功*/) {
        return SUCCESS; // C语言中的宏定义:#define SUCCESS 0
      }
      // ...
    }
    //使用举例
    int fd;
    int result = open(“c:\test.txt”, O_RDWR, S_IRWXU|S_IRWXG|S_IRWXO, &fd);
    if (result == SUCCESS) {
      // 取出fd使用
    } else if (result == EEXIST) {
      //...
    } else if (result == EACESS) {
      //...
    }
    
    // 错误码的返回方式二:函数返回打开的文件句柄,错误码放到errno中。
    int errno; // 线程安全的全局变量
    int open(const char *pathname, int flags, mode_t mode){
      if (/*文件不存在*/) {
        errno = EEXIST;
        return -1;
      }
      
      if (/*没有访问权限*/) {
        errno = EACCESS;
        return -1;
      }
      
      // ...
    }
    // 使用举例
    int hFile = open(“c:\test.txt”, O_RDWR, S_IRWXU|S_IRWXG|S_IRWXO);
    if (-1 == hFile) {
      printf("Failed to open file, error no: %d.\n", errno);
      if (errno == EEXIST ) {
        // ...        
      } else if(errno == EACCESS) {
        // ...    
      }
      // ...
    }
    
    2. 返回null值

    不太建议:

    1. 如果某个函数有可能返回null值而调用者在使用的时候没有做null判断处理,可能会导致空指针异常NPE;
    2. 如果我们定义了这种函数,代码中会充斥着大量NULL值判断逻辑,写起来繁琐而且和业务代码耦合,影响可读性。
    public class UserService {
      private UserRepo userRepo; // 依赖注入
      
      public User getUser(String telephone) {
        // 如果用户不存在,则返回null
        return null;
      }
    }
    
    // 使用函数getUser()
    User user = userService.getUser("18917718965");
    if (user != null) { // 做NULL值判断,否则有可能会报NPE
      String email = user.getEmail();
      if (email != null) { // 做NULL值判断,否则有可能会报NPE
        String escapedEmail = email.replaceAll("@", "#");
      }
    }
    

    那是否可以用异常代替呢?

    对于get、find、query之类的函数,没找到就是返回null并不是异常。null应该更合理。当然这个事情还是要整个项目的规范一致。

    对于查找这种函数,处理返回数据对象,还会返回下标位置。比如Java的indexOf函数。这个时候就不好用null表示值不存在的情况(对比找数据,这个是找位置);我们对于这种情况两种处理:1. 返回位置没找到的异常 2. 是返回一个特殊值,例如-1。-1更合理一点因为没找到不是异常而是正常情况。

    3. 返回空对象

    一个经典的策略是应用空对象设计模式,后面讲。

    对于比较简单的情况,当返回的数据是字符串或者是空集合类型的时候, 可以用空字符串和空集合。这样我们在使用函数的时候就不需要做null值判断。

    // 使用空集合替代NULL
    public class UserService {
      private UserRepo userRepo; // 依赖注入
      
      public List<User> getUsers(String telephonePrefix) {
       // 没有查找到数据
        return Collections.emptyList();
      }
    }
    // getUsers使用示例
    List<User> users = userService.getUsers("189");
    for (User user : users) { //这里不需要做NULL值判断
      // ...
    }
    
    // 使用空字符串替代NULL
    public String retrieveUppercaseLetters(String text) {
      // 如果text中没有大写字母,返回空字符串,而非NULL值
      return "";
    }
    // retrieveUppercaseLetters()使用举例
    String uppercaseLetters = retrieveUppercaseLetters("wangzheng");
    int length = uppercaseLetters.length();// 不需要做NULL值判断 
    System.out.println("Contains " + length + " upper case letters.");
    
    4. 抛出异常对象

    最常见的还是抛出异常。异常会带有更多的信息,比如函数调用栈,而且异常处理和正常的逻辑还是分开的,所以可读性会好点。

    大部分动态语言只有运行时异常(非受检异常),而Java还有编译时异常,也叫受检异常。

    对于代码bug以及不可恢复的异常(数据库连接失败),即使我们捕获了也做不了什么,我们就用非受检异常;对于可恢复异常,比如提额现金大于余额,我们使用受检异常明确告知调用者需要捕获处理。

    当 Redis 的地址(参数 address)没有设置的时候,我们直接使用默认的地址(比如本地地址和默认端口);当 Redis 的地址格式不正确的时候,我们希望程序能 fail-fast,也就是说,把这种情况当成不可恢复的异常,直接抛出运行时异常,将程序终止掉。

    // address格式:"192.131.2.33:7896"
    public void parseRedisAddress(String address) {
      this.host = RedisConfig.DEFAULT_HOST;
      this.port = RedisConfig.DEFAULT_PORT;
      
      if (StringUtils.isBlank(address)) {
        return;
      }
    
      String[] ipAndPort = address.split(":");
      if (ipAndPort.length != 2) {
        throw new RuntimeException("...");
      }
      
      this.host = ipAndPort[0];
      // parseInt()解析失败会抛出NumberFormatException运行时异常
      this.port = Integer.parseInt(ipAndPort[1]);
    }
    
    如何处理抛出的异常
    • 直接吞掉
    public void func1() throws Exception1 {
      // ...
    }
    
    public void func2() {
      //...
      try {
        func1();
      } catch(Exception1 e) {
        log.warn("...", e); //吐掉:try-catch打印日志
      }
      //...
    }
    
    • 原封不动re-throw
    public void func1() throws Exception1 {
      // ...
    }
    
    public void func2() throws Exception1 {//原封不动的re-throw Exception1
      //...
      func1();
      //...
    }
    
    • 包装成新的异常然后抛出
    public void func1() throws Exception1 {
      // ...
    }
    
    public void func2() throws Exception2 {
      //...
      try {
        func1();
      } catch(Exception1 e) {
       throw new Exception2("...", e); // wrap成新的Exception2然后re-throw
      }
      //...
    }
    

    当我们面对异常的时候,

    • 如果func1抛出的异常是可恢复的,而且func2的调用方不关心此异常,那么我们可以在func2内将func1的异常吞掉;
    • 如果func1的异常对于func2的调用方来说,可以理解、关心的,并且在业务概念上有一定的相关性,可以选择继续抛出;
    • 如func1的异常太底层,func2的调用方缺乏背景去理解、业务无关,我们就把他包装成调用方可以理解的新异常,re-throw。

    是否往上继续抛出,要看上层代码是否关心这个异常。关心就将它抛出,否则就直接吞掉。是否需要包装成新的异常抛出,看上层代码是否能理解这个异常、是否业务相关。如果能理解、业务相关就可以直接抛出,否则就封装成新的异常抛出。

    重构ID生成器项目中各函数的异常处理代码

    public class RandomIdGenerator implements IdGenerator {
      private static final Logger logger = LoggerFactory.getLogger(RandomIdGenerator.class);
    
      @Override
      public String generate() {
        String substrOfHostName = getLastFiledOfHostName();
        long currentTimeMillis = System.currentTimeMillis();
        String randomString = generateRandomAlphameric(8);
        String id = String.format("%s-%d-%s",
                substrOfHostName, currentTimeMillis, randomString);
        return id;
      }
    
      private String getLastFiledOfHostName() {
        String substrOfHostName = null;
        try {
          String hostName = InetAddress.getLocalHost().getHostName();
          substrOfHostName = getLastSubstrSplittedByDot(hostName);
        } catch (UnknownHostException e) {
          logger.warn("Failed to get the host name.", e);
        }
        return substrOfHostName;
      }
    
      @VisibleForTesting
      protected String getLastSubstrSplittedByDot(String hostName) {
        String[] tokens = hostName.split("\\.");
        String substrOfHostName = tokens[tokens.length - 1];
        return substrOfHostName;
      }
    
      @VisibleForTesting
      protected String generateRandomAlphameric(int length) {
        char[] randomChars = new char[length];
        int count = 0;
        Random random = new Random();
        while (count < length) {
          int maxAscii = 'z';
          int randomAscii = random.nextInt(maxAscii);
          boolean isDigit= randomAscii >= '0' && randomAscii <= '9';
          boolean isUppercase= randomAscii >= 'A' && randomAscii <= 'Z';
          boolean isLowercase= randomAscii >= 'a' && randomAscii <= 'z';
          if (isDigit|| isUppercase || isLowercase) {
            randomChars[count] = (char) (randomAscii);
            ++count;
          }
        }
        return new String(randomChars);
      }
    }
    
    • 对于 generate() 函数,如果本机名获取失败,函数返回什么?这样的返回值是否合理?
    • 对于 getLastFiledOfHostName() 函数,是否应该将 UnknownHostException 异常在函数内部吞掉(try-catch 并打印日志)?还是应该将异常继续往上抛出?如果往上抛出的话,是直接把 UnknownHostException 异常原封不动地抛出,还是封装成新的异常抛出?
    • 对于 getLastSubstrSplittedByDot(String hostName) 函数,如果 hostName 为 NULL 或者是空字符串,这个函数应该返回什么?
    • 对于 generateRandomAlphameric(int length) 函数,如果 length 小于 0 或者等于 0,这个函数应该返回什么?

    重构generate函数

    如何处理空字符和null,本身是要看业务是否允许。不过就个人判断来讲,更倾向于抛出异常给调用者。

    public String generate() throws IdGenerationFailureException {
    String substrOfHostName = getLastFieldOfHostName();
    if (substrOfHostName == null || substrOfHostName.isEmpty()) {
      throw new IdGenerationFailureException("host name is empty.");
    }
    long currentTimeMillis = System.currentTimeMillis();
    String randomString = generateRandomAlphameric(8);
    String id = String.format("%s-%d-%s", substrOfHostName, currentTimeMillis, randomString);
    return id;
    }
    

    重构getLastFieldOfHostName函数

    private String getLastFieldOfHostName() throws UnknownHostException{
        String substrOfHostName = null;
        String hostName = InetAddress.getLocalHost().getHostName();
        substrOfHostName = getLastSubstrSplittedByDot(hostName);
        return substrOfHostName;
     }
    

    当主机名获取失败的时候,返回null还是会影响后续的逻辑,所以抛出一个异常会比较好。而上层调用者和他业务相关,所以也不需要对这个异常进行重新包装。

    这个函数的调用者(generate)获取到的UnknownHostException我们需要包装:

    • 上层调用者调用generate的时候,不知道细节,面向接口而非实现。所以在出异常的时候也不能暴露细节。
    • 从封装的角度,我们不希望低层的异常暴露给上层,调用者拿到这异常并不知道如何处理。
    • UnknownHostException和generate在业务上没有相关性。
    public String generate() throws IdGenerationFailureException {
    String substrOfHostName = null;
    try {
      substrOfHostName = getLastFieldOfHostName();
    } catch (UnknownHostException e) {
      throw new IdGenerationFailureException("host name is empty.");
    }
    long currentTimeMillis = System.currentTimeMillis();
    String randomString = generateRandomAlphameric(8);
    String id = String.format("%s-%d-%s",
            substrOfHostName, currentTimeMillis, randomString);
    return id;
    }
    

    重构 getLastSubstrSplittedByDot() 函数

    @VisibleForTesting
    protected String getLastSubstrSplittedByDot(String hostName) {
       String[] tokens = hostName.split("\\.");
       String substrOfHostName = tokens[tokens.length - 1];
       return substrOfHostName;
    }
    

    理论上hostName参数传递应该程序员来保证没有code bug。

    所以如果是私有方法你自己调用,你可以把握传递的参数是ok的,那就不需要做空值或者null的判断;反之是public,别人可以调用,他就有可能会会发生这种误用的情况。所以为了提高健壮性,需要加上。

    @VisibleForTesting
    protected String getLastSubstrSplittedByDot(String hostName) {
        if (hostName == null || hostName.isEmpty()) {
          throw IllegalArgumentException("..."); //运行时异常
        }
        String[] tokens = hostName.split("\\.");
        String substrOfHostName = tokens[tokens.length - 1];
        return substrOfHostName;
    }
    

    在调用者时候,我们也需要保证传参不是null或者空值,所以在getLastFieldOfHostName() 函数的代码也要作相应的修改

    private String getLastFieldOfHostName() throws UnknownHostException{
        String substrOfHostName = null;
        String hostName = InetAddress.getLocalHost().getHostName();
        if (hostName == null || hostName.isEmpty()) { // 此处做判断
          throw new UnknownHostException("...");
        }
        substrOfHostName = getLastSubstrSplittedByDot(hostName);
        return substrOfHostName;
     }
    

    重构 generateRandomAlphameric() 函数

      @VisibleForTesting
      protected String generateRandomAlphameric(int length) {
        char[] randomChars = new char[length];
        int count = 0;
        Random random = new Random();
        while (count < length) {
          int maxAscii = 'z';
          int randomAscii = random.nextInt(maxAscii);
          boolean isDigit= randomAscii >= '0' && randomAscii <= '9';
          boolean isUppercase= randomAscii >= 'A' && randomAscii <= 'Z';
          boolean isLowercase= randomAscii >= 'a' && randomAscii <= 'z';
          if (isDigit|| isUppercase || isLowercase) {
            randomChars[count] = (char) (randomAscii);
            ++count;
          }
        }
        return new String(randomChars);
      }
    }
    

    对于入参length,非正数的时候应该怎么处理,首先负数肯定是不合理的,所以可以抛出异常,而0可以看业务需要,不过这种corner case不管是把他当成异常还是正常输入,在注释说明是很有必要的。

    重构之后的完整代码

    public class RandomIdGenerator implements IdGenerator {
      private static final Logger logger = LoggerFactory.getLogger(RandomIdGenerator.class);
    
      @Override
      public String generate() throws IdGenerationFailureException {
        String substrOfHostName = null;
        try {
          substrOfHostName = getLastFieldOfHostName();
        } catch (UnknownHostException e) {
          throw new IdGenerationFailureException("...", e);
        }
        long currentTimeMillis = System.currentTimeMillis();
        String randomString = generateRandomAlphameric(8);
        String id = String.format("%s-%d-%s",
                substrOfHostName, currentTimeMillis, randomString);
        return id;
      }
    
      private String getLastFieldOfHostName() throws UnknownHostException{
        String substrOfHostName = null;
        String hostName = InetAddress.getLocalHost().getHostName();
        if (hostName == null || hostName.isEmpty()) {
          throw new UnknownHostException("...");
        }
        substrOfHostName = getLastSubstrSplittedByDot(hostName);
        return substrOfHostName;
      }
    
      @VisibleForTesting
      protected String getLastSubstrSplittedByDot(String hostName) {
        if (hostName == null || hostName.isEmpty()) {
          throw new IllegalArgumentException("...");
        }
    
        String[] tokens = hostName.split("\\.");
        String substrOfHostName = tokens[tokens.length - 1];
        return substrOfHostName;
      }
    
      @VisibleForTesting
      protected String generateRandomAlphameric(int length) {
        if (length <= 0) {
          throw new IllegalArgumentException("...");
        }
    
        char[] randomChars = new char[length];
        int count = 0;
        Random random = new Random();
        while (count < length) {
          int maxAscii = 'z';
          int randomAscii = random.nextInt(maxAscii);
          boolean isDigit= randomAscii >= '0' && randomAscii <= '9';
          boolean isUppercase= randomAscii >= 'A' && randomAscii <= 'Z';
          boolean isLowercase= randomAscii >= 'a' && randomAscii <= 'z';
          if (isDigit|| isUppercase || isLowercase) {
            randomChars[count] = (char) (randomAscii);
            ++count;
          }
        }
        return new String(randomChars);
      }
    }
    


沪ICP备19023445号-2号
友情链接