设计模式学习笔记 - 规范与重构 - 8.实践:程序出错返回啥?NULL、异常、错误吗、空对象?重构ID生成器,处理各函数的异常
概述
我们可以把函数的运行结果分为两类。一类是预期结果,也就是正常情况下输出的结果。一类是非预期的结果,也就是函数在异常(或出错)情况下输出的结果。
在正常情况下,函数返回数据的类型非常明确,但是在异常情况下,函数的返回数据类型确非常灵活,有多种选择,比如异常(Exception)、错误码、NULL 值、特殊值(比如 -1)、空对象(比如空字符串、空集合)等。
在异常情况下,函数到底该返回什么样的数据类型,并不那么容易判断。比如,上节课中,本机名获取失败的时候, ID 生成器的 generate()
函数应该返回什么呢? 是异常?空字符串?还是 NULL 值?又或者是其他特殊值呢?
程序出错返回啥?NULL、异常、错误吗、空对象?
从 ID 生成器代码讲起
上篇《规范与重构 - 7.实践:通过一段ID生成器代码,学习如何发现代码质量问题》我们把一份 ID 生成器代码从 “能用” 重构成了 “好用”。最终给出的代码看似以及完美了,但是如果再用心推敲以下,代码中关于出错处理的方式,还有进一步优化的空间,值得我们拿出来再讨论下。
下面是上节课的代码。
public class RandomIdGenerator implements LogTraceIdIdGenerator {private static final Logger logger = LoggerFactory.getLogger(RandomIdGenerator.class);@Overridepublic String generate() {String substrOfHostName = getLastFieldOfHostName();long currentTimeMillis = System.currentTimeMillis();String randomString = generateRandomAlphameric(8);String id = String.format("%s-%d-%s",substrOfHostName, currentTimeMillis, randomString);return id;}private String getLastFieldOfHostName() {String substrOfHostName = null;try {String hostName = InetAddress.getLocalHost().getHostName();substrOfHostName = getLastSubstrSplitByDot(hostName);} catch (UnknownHostException e) {logger.error("failed to get the host name.", e);}return substrOfHostName;}@VisibleForTestingprotected String getLastSubstrSplitByDot(String hostName) {String[] tokens = hostName.split("\\.");String substrOfHostName = tokens[tokens.length - 1];return substrOfHostName;}@VisibleForTestingprotected String generateRandomAlphameric(int length) {char[] randomChars = new char[length];int count = 0;Random random = new Random();while (count < length) {int randomAscii = random.nextInt(122);boolean isDigit = randomAscii >= 48 && randomAscii <= 57;boolean isUpperCase = randomAscii >= 65 && randomAscii <= 90;boolean isLowerCase = randomAscii >= 97 && randomAscii <= 122;if (isDigit || isUpperCase || isLowerCase) {randomChars[count++] = (char) randomAscii;}}return new String(randomChars);}
}
这段代码中的四个函数的出错处理方式,总结出下面这样几个问题:
- 对于
generate()
函数,如果本机名获取失败,函数返回什么?这样的返回值是否合理? - 对于
getLastFieldOfHostName()
函数,是否应该将UnknownHostException
异常在函数内部吞掉(try-catch 并打印日志)?还是应该将异常抛出?如果抛出的话,是直接把UnknownHostException
原封不动的抛出,还是封装成新的异常抛出? - 对于
getLastSubstrSplitByDot()
函数,如果 hostName 为 NULL 或者是空字符串,这个函数应该返回什么? - 对于
generateRandomAlphameric()
函数,如果 length 小于或等于 0,这个函数应该返回什么?
函数出错应该返回啥?
函数出错返回的数据,一共有四种情况:错误码、NULL 值、空对象、异常对象。接下来,我们一一分析下。
1.返回错误码
C 语言中没有异常这样的语法机制,因此,返回错误码便是最常用的出错处理方式。而在 Java 等比较新的编程语言中,大部分情况下,都用异常来处理函数出错的情况,极少会用到错误码。
在 C 语言中,错误码的返回方式有两种:一种是直接占用函数的返回值,函数正常执行的返回值方到出参中;另一种是将错误码定义为全局变量,在函数执行出错时,函数调用者通过这个全局变量来获取错误码。我举个例子进一步解释。
// 错误码的返回方式一:占用函数的返回值
int open(const char *pathname, int flags, mode_t mode, int *fd) {if(/*文件不存在*/) {return EEXISTS;}if(/*没有访问权限*/) {return EACCESS;}if(/*打开文件成功*/) {return SUCCESS;}// ...
}
// 使用举例
int result = open("c:\test.text", O_RDWR, S_IRWXU|S_IRWXG|S_IRWXO, &fd);
if(result == SUCCESS) {// 取出fd使用
} else if(result == EEXISTS) {// ...
} else if(result == EACCESS) {// ...
}// 错误码的返回方式二
int error; // 线程安全的全局变量
int open(const char *pathname, int flags, mode_t mode) {if(/*文件不存在*/) {error = EEXISTS;return -1;}if(/*没有访问权限*/) {error = EACCESS;return -1;}// ...
}
// 使用举例
int hFile = open("c:\test.text", O_RDWR, S_IRWXU|S_IRWXG|S_IRWXO);
if(hFile == -1) {if(error == EEXISTS) {// ...} else if(error == EACCESS) {// ...}//...
}
如果你熟悉的编程语言中有异常这种语法机制,那就尽量不要使用错误码。异常相对于错误码,有诸多优势,比如可以携带详细的错误信息(exception 中可以有 message、stack trace 等信息)。关于异常,我们待会还会非常详细的讲解。
2.返回 NULL 值
在多数编程语言中,我们用 NULL 来表示 “不存在” 这种语义。不过,很多人不建议函数返回 NULL 值,认为这是一种不好的设计思路,主要理由有以下两个。
- 如果某个函数返回 NULL 值,我们在使用它的时候,忘记了做 NULL 值判断,就有可能会抛出空指针异常(NULL Point Exception,缩写为 NPE)。
- 如果我们定义了很多返回值可能为 NULL 的函数,那代码中就会充斥着大量的 NULL 值判断逻辑,一方面写起来比较繁琐,另一方面它们跟正常的业务逻辑耦合在一起,会影响代码的可读性。
// 使用函数 getUser()
User user = userService.getUser("18147452144");
if (user != null) { // 做NULL判断,否则可能会报 NPEString email = user.getEmail();if (email != null) { // 做NULL判断,否则可能会报 NPEString escapedEmail = email.replaceAll("@", "#");}
}
那我们是否可以用异常替代 NULL 值,在查找用户不存在的时候,让函数抛出 UserNotFoundException
异常呢?
个人觉得,尽管返回 NULL 值有诸多弊端,但是对于以 get、find、select、search、query 等单词开头的查找函数来说,数据不存在,并非是一种异常情况,这是一种正常行为。所以,返回代表不存在语义的 NULL值比返回更加合理。
其实上面将的理由,也不是特别有说服力。对于查找数据不存在的情况,函数到底是返回 NULL 还是异常,有一个比较重要的参考标准是,看项目中其他类似查找函数都是如何定义的,只要整个项目遵从两种中的任何一种都可以。你只需要在函数定义的地方解释清楚,让调用者清晰地知道数据不存在的时候会返回什么就可以了。
再补充一点,对于查找函数来说,除了返回数据对象之外,有的还会返回下标位置,比如 java 中的 indexOf()
函数,用来实现某个字符串查找另一个子串第一次出现的位置。函数的返回值类型为基本类型 int
。这个时候,我们就无法用 NULL 值来表示不存在的情况了。这种情况,我们有两种处理思路,一种是返回 NotFoundException
,一种是返回一个特殊值,比如 -1。不过,显然 -1 更加合理,理由也是同样的,也就是说,“没有查找到” 是一种正常行而非异常的行为。
3.返回空对象
刚刚降到,返回 NULL 值有各种弊端。应对这个问题有一个比较经典的策略,那就是应用空对象设计模式(Null Object Design Pattern)。关于这个设计模式,后面会讲到。不过,今天来讲比较简单、比较特殊的空对象,那就是空字符串和空集合。
当函数返回的数据是字符串类型或者集合类型的时候,我们可以用空字符串或空集合替代 NULL 值,来表示不存在的情况。这样,我们在使用函数的时候,就可以不用做 NULL 值判断。
// 使用空集合替代NULL
public class UserService {private UserRepo userRepo; // 依赖注入public List<User> getUsers(String telephonePrefix) {// 没有查找到数据return Collections.emptyList();}
}
// 使用函数 getUser()
List<User> users = userService.getUsers("181");
for (User user : users) { // 这里不需要做NULL值判断//...
}// 使用空字符串替代NULL
public String retrieveUppercaseLetters(String text) {// 如果text中没有大写字母,返回空字符串,而非NULL值return "";
}
// retrieveUppercaseLetters() 使用举例
String uppercaseLetters = retrieveUppercaseLetters("abc");
int length = uppercaseLetters.length(); // 不需要做NULL值判断
System.out.println("Contains " + length + " upper case letters");
4.抛出异常对象
受检异常和非受检异常
最常用的函数出错处理方式就是抛出异。异常可以携带更多的错误信息,比如函数调用栈信息。此外,异常可以将正常逻辑和异常逻辑的处理分离开,这样代码的可读性就会更好。
Java 除了运行时异常(Runtime Exception)外,还定义了另一种异常类型,编译时异常(Compile Exception)。
- 对于运行时异常,在编写代码的时候,可以不用主动去
try-catch
,编译器在编译时并不会检查代码是否对运行时异常做了处理。 - 相反,编译时异常,在编写代码时需要主动去
try-catch
或者在函数定义中申明,否则编译就会报错。
所以,运行时异常也叫做非受检异常(Unchecked Exception),编译时异常也叫作受检异常(Checked Exception)。
在 Java 中,定义了两种异常类型,那再异常出现时,我们应该选择抛出哪种异常类型呢?是受检异常还是非受检异常?
对于代码 bug(比如数组越界)以及不可恢复异常(比如数据库连接失败),即便捕获了也做不了太多事情,所以,我们倾向于使用非受检异常。对于可恢复异常、业务异常,比如提现金额大于余额的异常,我们更倾向于使用受检异常,明确告知调用者需要捕获处理。
举个例子来解释下,代码如下所示。当 Redis 的地址(参数 address)没有设置的时候,我们直接使用默认的地址(比如本地地址和默认端口);当 Redis 的地址格式不正确的时候,我们希望程序员能 fail-fast
,也就是说,把这种情况当成不可恢复的异常,直接抛出运行时异常,将程序终止掉。
public class RedisProcessor {private String host;private Integer port;// address格式:"192.168.1.105: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]);}
}
实际上,Java 支持的受检异常一直被人诟病,很多人主张所有的异常情况都该使用非受检异常。支持这种观点的理由主要有三个。
- 受检异常需要显式地在函数中定义声明。如果函数会抛出很多受检异常,那函数的定义就会非常冗长,这就会影响代码的可读性,使用起来不方便。
- 编译器强制我们必须显示地捕获所有的受检异常,代码实现会比较繁琐。而非受检异常正好相反,不需要在定义中显示生命,并且是否捕获处理,也可以自由决定。
- 受检异常的使用违反开闭原则。如果我们给某个函数新增一个受检异常,这个函数所在的函数调用链上的所有位于其之上的函数都需要做相应的代码修改,直到调用链中的某个函数将这个新增的异常
try-catch
处理掉为止。而新增非受检异常可以不改动调用链上的代码,我们可以灵活地选择在某个函数中集中处理,比如在 Spring 中的 AOP 切面中集中处理异常。
其实,非受检异常也有弊端,它的优点其实也正是它的缺点。非受检异常非常灵活,怎么处理的主动权交给了程序员。前面我们讲过,过于灵活会带来不可控,非受检异常不需要显式地在函数中定义申明,那我们在使用函数的时候,就需要查看代码才能知道具体会抛出哪些异常。非受检异常不需要强制捕获处理,那程序员就有可能漏掉一些本应该捕获处理的异常。
对于应该用受检异常还是非受检异常,争议有很多,但并没有一个强有力的理由能够说明一个就一定比另一个更好。所以,我们只需根据团队的开发习惯,在同一个项目中,制定统一的异常处理规范即可。
如何处理函数抛出的异常
一般有下面三种处理方法。
1.直接吞掉
public void func1() throws Exception1 { /*...*/ }public void func2() {// ...try {func1();} catch (Exception1 e) {log.warn("...", e); // 吞掉:try-catch打印日志}// ...
}
2.原封不动地 re-throw
public void func1() throws Exception1 { /*...*/ }//原封不动的re-throw Exception1
public void func2() throws Exception1 {// ...func1();// ...
}
3.包装成新的异常 re-throw
public void func1() throws Exception1 { /*...*/ }public void func2() {// ...try {func1();} catch (Exception1 e) {throw new Exception2(e); // wrap成新的Exception2,然后re-throw}// ...
}
当我们面对函数抛出异常时,应该选择上面的哪种处理方式呢? 我总结了下面三个参考原则:
- 如果
func1()
抛出的异常是可以恢复,且func2()
的调用方并不关心此异常,我们完全可以在func2()
内将func1()
抛出的异常吞掉。 - 如果
func1()
抛出的异常对func2()
调用方来说,是可以理解的、关心的,并且在业务概念上有一定的相关性,我们可以直接选择将func1()
抛出的异常 re-throw。 - 如果
func1()
抛出的异常太底层,对func2()
的调用方来说,缺乏背景去理解、且业务概念上无关,我们可以将它重新包装成调用方可以理解的异常,然后 re-throw。
总之,是否网上继续抛出,要看上层代码是否关心这个异常。关系就将它抛出,否则就直接吞掉。是否需要包装成新的异常抛出,看上层代码是否理解这个异常、是否业务相关。如果能理解、业务相关就可以直接抛出,否则就封装成新的异常抛出。
程序出错返回知识回顾
1.返回错误码
C语言没有异常这样的语法机制,返回错误码便是最常用的出错处理方式。而 Java 等比较新的编程语言中,大部分情况下,都抛出异常来处理程序出错的情况,极少会用到错误码。
2.返回 NULL 值
大多数编程语言中,都用 NULL 值表示 “不存在” 这种语义。对于查找函数来说,数据不存在并非一种异常情况,是一种正常行为,所以返回表示不存在语义的 NULL 值比返回异常更加合理。
3.返回空对象
返回 NULL 值有各种弊端,对此有一个比较经典的应对策略,那就是应用空对象设计模式。当函数返回的数据是字符串或集合类型时,我们可以使用空对象或空集合替代 NULL 值,来表示不存在的情况。这样,我们在使用的时候,就可以不用做 NULL 值判断。
4.抛出异常对象
尽管前面讲了很多函数出错的返回数据类型,但是,最常用的函数出错处理方式是抛出异常。异常有两种类型:受检异常和非受检异常。
对于应用受检异常还是非受检异常,争论有很多,但也并没有一个非常强有力的理由,说明一个就比另一个更好。所以,我们只需要根据团队的开发习惯,在同一个项目中,制定统一的异常处理规范即可。
对于函数抛出的异常,我们有三种处理方式:直接吞掉、直接向上抛出、包装成新的异常抛出。
重构ID生成器,处理各函数的异常
我们在进行软件设计的时候,除了要保证正常情况下的逻辑运行正确之外,还需要编写大量额外的代码,来处理可能出现的异常情况,以保证代码在任何情况下,都在我们的掌控之内,不会出现非预期的运行结果。程序的 bug 往往出现在一些边界条件和异常情况下,所以说,异常处理得好坏直接影响了代码的健壮性。全面、合理地处理各种异常能有效地减少代码 bug,也是保证代码质量的一个重要手段。
重构 generate() 函数
对于 generate()
函数,如果本机名获取失败,函数安徽什么?这样的返回值是否合理?
public String generate() {String substrOfHostName = getLastFieldOfHostName();long currentTimeMillis = System.currentTimeMillis();String randomString = generateRandomAlphameric(8);String id = String.format("%s-%d-%s",substrOfHostName, currentTimeMillis, randomString);return id;
}
ID 由三部分构成:本机名、时间戳和随机数。时间戳和随机数的生成函数不会出错,主机名有可能获取失败。在目前的代码中,如果主机名获取失败,substrOfHostName 为 NULL,那 generate()
会返回类似 “null-1710480822005-33Ab3uK6” 这样的数据。如果主机名获取失败, substrOfHostName 为空字符串,那 generate()
会返回类似 “-1710480822005-33Ab3uK6” 这样的数据。
在异常情况下,返回上面两种特殊的 ID 数据格式,这样的做法是否合理呢? 这其实很难讲,我们要看具体的业务是怎么设计的。不过,个人更倾向于将异常告知调用者。所以,这里最好是抛出受检异常,而非特殊值。
按照这个思路,我们对 generate()
函数进行重构。重构之后的代码如下所示:
public String generate() throws IdGenerationFailureException {String substrOfHostName = getLastFieldOfHostName();if (substrOfHostName == null || substrOfHostName.isEmpty()) {throw new IdGenerationFailureException();}long currentTimeMillis = System.currentTimeMillis();String randomString = generateRandomAlphameric(8);String id = String.format("%s-%d-%s",substrOfHostName, currentTimeMillis, randomString);return id;}
重构 getLastFieldOfHostName() 函数
对 getLastFieldOfHostName()
函数,是否应该将 UnknownHostException
异常在函数内部吞掉,还是应该将异常继续网上抛出?如果网上抛出的话,是直接把 UnknownHostException
异常原封不动的抛出,还是封装成新的异常抛出?
private String getLastFieldOfHostName() {String substrOfHostName = null;try {String hostName = InetAddress.getLocalHost().getHostName();substrOfHostName = getLastSubstrSplitByDot(hostName);} catch (UnknownHostException e) {logger.error("failed to get the host name.", e);}return substrOfHostName;}
现在的处理方式是当主机名获取失败的时候,getLastFieldOfHostName()
函数返回 NULL 值。我们前面讲过,是返回 NULL 值还是异常对象,要看获取不到数据是正常行为还是异常行为。获取主机名失败会影响后续逻辑处理,并不是我们期望的,所以它是一种异常行为。这里最好是抛出异常,而非返回 NULL 值。
至于是将 UnknownHostException
异常抛出,还是重新封装成新的异常抛出,要看函数跟异常是否有业务相关性。getLastFieldOfHostName()
函数用来获取主机名的最后一个字段, UnknownHostException
异常表示主机名获取失败,两者算是业务相关,所以可以直接将 UnknownHostException
抛出,不需要重新包裹新的异常。
按照上面的思路,我们对代码进行重构。
private String getLastFieldOfHostName() throws UnknownHostException {String substrOfHostName = null;String hostName = InetAddress.getLocalHost().getHostName();substrOfHostName = getLastSubstrSplitByDot(hostName);return substrOfHostName;}
getLastFieldOfHostName()
函数修改之后, generate()
函数也要做相应的修改。我们需要在 generate()
函数中捕获 getLastFieldOfHostName()
函数抛出的 UnknownHostException
异常。当捕获到异常之后,应该怎么处理呢?
按照之前的分析, ID 获取失败的时候,要明确的告知调用者。所以,我们不能再 generate()
函数中,将 UnknownHostException
异常吞掉。那是应该原封不动的抛出,还是封装成新的异常抛出呢?
需要选择后者。在 generate()
函数中,我们需要捕获 UnknownHostException
异常,并包裹成新的异常 IdGenerationFailureException
网上抛出。这么的原因有三个:
- 调用者在使用
generate()
函数时,只需要知道它生成的是随机唯一 ID,并不关心 ID 是如何生成的。即这是依赖抽象而非实现编程。如果generate()
函数直接抛出UnknownHostException
异常,实际上是暴露了实现细节。 - 从代码封装的角度来说,我们不希望将
UnknownHostException
异常这个比较底层的异常,暴露给更上层的代码。而且,调用者拿到这个异常时,并不能理解这个异常到底代表了什么,也不知道该如何处理。 UnknownHostException
异常跟generate()
函数,在业务概念上没有相关性。
按照上面的设计思路,我们对 generate()
函数再次进行重构。
public String generate() throws IdGenerationFailureException {String substrOfHostName = null;try {substrOfHostName = getLastFieldOfHostName();} catch (UnknownHostException e) {throw new IdGenerationFailureException();}long currentTimeMillis = System.currentTimeMillis();String randomString = generateRandomAlphameric(8);String id = String.format("%s-%d-%s",substrOfHostName, currentTimeMillis, randomString);return id;}
重构 getLastSubstrSplittedByDot() 函数
对于 getLastSubstrSplittedByDot()
函数,如果 hostName 为 NULL 或者空字符串,这个函数应该返回什么?
@VisibleForTestingprotected String getLastSubstrSplitByDot(String hostName) {String[] tokens = hostName.split("\\.");String substrOfHostName = tokens[tokens.length - 1];return substrOfHostName;}
理论上讲,参数传递的正确性应该由程序员保证,我们无需做 NULL 值或空字符串的判断。但是,话说回来,谁也保证不了程序员就一定不会传递 NULL 值或者空字符串。我们到底该不该做 NULL 值或空字符串的判断呢?
如果函数是 private 私有的,只在类内部被调用,完全在你自己的掌控之下,自己保证在调用这个 private 函数的时候,不要传递 NULL 值或者空字符串就可以了。所以,可以不在 private 函数中做 NULL 值或者空字符串的判断。如果是 public 的,你无法掌控会被谁调用以及如何调用,为了尽可能提高代码的健壮性,我们最好是在 public 函数中做 NULL 值或者空字符串的判断。
getLastSubstrSplittedByDot()
是 protected 的,既不是 private 函数,也不是 public 函数,那要不要做 NULL 值或者空字符串的判断逻辑。虽然加上有些冗余,但多加些校验总归是不错的。
按照这个设计思路,我们对 getLastSubstrSplittedByDot()
函数进行重构。
@VisibleForTestingprotected String getLastSubstrSplitByDot(String hostName) {if (hostName == null || hostName.isEmpty()) {throw new 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 IllegalArgumentException("..."); //此处做判断}substrOfHostName = getLastSubstrSplitByDot(hostName);return substrOfHostName;}
重构 generateRandomAlphameric() 函数
对于 generateRandomAlphameric()
函数,如果 length <= 0,这个函数应该返回什么?
@VisibleForTestingprotected String generateRandomAlphameric(int length) {char[] randomChars = new char[length];int count = 0;Random random = new Random();while (count < length) {int randomAscii = random.nextInt(122);boolean isDigit = randomAscii >= 48 && randomAscii <= 57;boolean isUpperCase = randomAscii >= 65 && randomAscii <= 90;boolean isLowerCase = randomAscii >= 97 && randomAscii <= 122;if (isDigit || isUpperCase || isLowerCase) {randomChars[count++] = (char) randomAscii;}}return new String(randomChars);}
我们先来看一下 length < 0
的情况。生成一个长度为负值的随机字符串是不合常规逻辑的,是一种异常行为。所以,当出传入的参数 length < 0 的时候,我们抛出 IllegalArgumentException
异常。
我们再来看下 length = 0
的情况。length = 0
是否是异常行为呢?这就看你自己怎么定义了。我们既可以把它定义为一种异常行为,抛出 IllegalArgumentException
异常,也可以把它定义为一种正常行为,让函数入参 length = 0
的情况下,直接返回字符串。不管选择哪种处理方式,最关键的一点是,要在函数注释中,明确告知 length = 0
的情况下,会返回什么样的数据。
@VisibleForTestingprotected 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 randomAscii = random.nextInt(122);boolean isDigit = randomAscii >= 48 && randomAscii <= 57;boolean isUpperCase = randomAscii >= 65 && randomAscii <= 90;boolean isLowerCase = randomAscii >= 97 && randomAscii <= 122;if (isDigit || isUpperCase || isLowerCase) {randomChars[count++] = (char) randomAscii;}}return new String(randomChars);}
重构之后的 RandomIdGenerator 代码
public class RandomIdGenerator implements LogTraceIdIdGenerator {private static final Logger logger = LoggerFactory.getLogger(RandomIdGenerator.class);@Overridepublic String generate() throws IdGenerationFailureException {String substrOfHostName = null;try {substrOfHostName = getLastFieldOfHostName();} catch (UnknownHostException e) {throw new IdGenerationFailureException();}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 IllegalArgumentException("..."); //此处做判断}substrOfHostName = getLastSubstrSplitByDot(hostName);return substrOfHostName;}@VisibleForTestingprotected String getLastSubstrSplitByDot(String hostName) {if (hostName == null || hostName.isEmpty()) {throw new IllegalArgumentException("..."); //运行时异常}String[] tokens = hostName.split("\\.");String substrOfHostName = tokens[tokens.length - 1];return substrOfHostName;}@VisibleForTestingprotected 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 randomAscii = random.nextInt(122);boolean isDigit = randomAscii >= 48 && randomAscii <= 57;boolean isUpperCase = randomAscii >= 65 && randomAscii <= 90;boolean isLowerCase = randomAscii >= 97 && randomAscii <= 122;if (isDigit || isUpperCase || isLowerCase) {randomChars[count++] = (char) randomAscii;}}return new String(randomChars);}
}
重构 ID 生成器的总结
这里总结了三点经验:
- 在简单的代码,看上去再完美,只要我们下功夫去推敲,总有可以优化的空间,就看你愿不愿意把事情做到极致。
- 如果你内功不够深厚,理论知识不够扎实,那你就很难参透开源项目的代码到底优秀在哪里。就像如果我们没有之前的理论学习,没有今天一点一点重构、分析,只是给出最后重构好的
RandomIdGenerator
代码,你真的能学到它的设计精髓吗? - 对比最开始小王写的
IdGenerator
代码和最终的RandomIdGenerator
,它们一个是能用,一个是好用,天壤之别。作为程序员,我们对代码要有追求哈。
相关文章:

设计模式学习笔记 - 规范与重构 - 8.实践:程序出错返回啥?NULL、异常、错误吗、空对象?重构ID生成器,处理各函数的异常
概述 我们可以把函数的运行结果分为两类。一类是预期结果,也就是正常情况下输出的结果。一类是非预期的结果,也就是函数在异常(或出错)情况下输出的结果。 在正常情况下,函数返回数据的类型非常明确,但是…...

【Python使用】python高级进阶知识md总结第4篇:静态Web服务器-命令行启动动态绑定端口号,html 的介绍【附代码文档】
python高级进阶全知识知识笔记总结完整教程(附代码资料)主要内容讲述:操作系统,虚拟机软件,Ubuntu操作系统,Linux内核及发行版,查看目录命令,切换目录命令,绝对路径和相对…...

langchain+chatglm3+BGE+Faiss Linux环境安装依赖
前言 本篇默认读者已经看过之前windows版本,代码就不赘述,本次讲述是linux环境配置 超短代码实现!!基于langchainchatglm3BGEFaiss创建拥有自己知识库的大语言模型(准智能体)本人python版本3.11.0(windows环境篇&…...

Kubernetes kafka系列 | k8s部署kafka+zookeepe集群(可外部通信)
直通车 zookeeper搭建 请参考yaml kafka.yaml --- apiVersion: v1 kind: Service metadata:name: kafka-hslabels:app: kafka spec:ports:- port: 9092targetPort: 9092name: serverclusterIP: Noneselector:app: kafka --- apiVersion: v1 kind: Service metadata:name: ka…...

IDEA创建Sping项目只能勾选17和21,没有Java8?
解决办法: 替换创建项目的源 我们只知道IDEA页面创建Spring项目,其实是访问spring initializr去创建项目。故我们可以通过阿里云国服去间接创建Spring项目。将https://start.spring.io/或者http://start.springboot.io/替换为 https://start.aliyun.com/...

ChatGPT 遇到对手:Anthropic Claude 语言模型的崛起
ChatGPT 遇到对手:Anthropic Claude 语言模型的崛起 。 这个巨大的上下文容量使 Claude 2.1 能够处理更大的数据体。用户可以提供复杂的代码库、详细的财务报告或广泛的作品作为提示。然后 Claude 可以连贯地总结长文本,基于文档进行彻底的问答&#x…...

【LeetCode热题100】148. 排序链表(链表)
一.题目要求 给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。 二.题目难度 中等 三.输入样例 示例 1: 输入:head [4,2,1,3] 输出:[1,2,3,4] 示例 2: 输入:head [-1,5,3,4,0] 输…...
Ubuntu Linux - Primavera P6 EPPM 安装及分享
引言 根据计划,近日我制作了基于Ubuntu Linux 的P6虚拟机环境,同样里面包含了全套P6 最新版应用服务 此虚拟机仅用于演示、培训和测试目的。如您在生产环境中使用此虚拟机,请先与Oracle Primavera销售代表取得联系,以获取所需的应…...

微信小程序开发学习笔记——3.11完成form评论案例的实现逻辑
>>跟着b站up主“咸虾米_”学习微信小程序开发中,把学习记录存到这方便后续查找。 课程连接:https://www.bilibili.com/video/BV19G4y1K74d?p25&vd_source9b149469177ab5fdc47515e14cf3cf74 一、javascript参考手册——splice https://www.…...

Linux/Ubuntu/Debian控制台启动的程序和terminal分离的方法-正在运行怎么关闭窗口
disown 是一个 shell 内置函数,它从 shell 的作业表中删除指定的作业,使它们免受挂起的影响。 使用方法如下: 首先,正常运行命令: 你的命令然后,按 Ctrl Z 暂停命令。 现在,运行ÿ…...

Lua-Lua与C的交互3
Lua与C的交互是指在Lua脚本中调用C语言编写的函数或者在C语言中调用Lua脚本中定义的函数。这种交互可以实现Lua和C语言之间的数据传递和函数调用。 Lua提供了一组API函数,可以在C语言中使用这些函数来与Lua进行交互。通过这些API函数,C语言可以将数据传…...

TensorFlow的介绍和简单案例
TensorFlow是一个开源的机器学习框架,由Google开发和维护。它旨在使构建和训练机器学习模型变得更加容易,同时提供高度灵活性和可扩展性。 TensorFlow基于数据流图的概念。数据流图是一个由节点和边组成的有向图,其中节点表示操作,边表示数据的流动。TensorFlow通过在数据…...

基于Java+SpringMVC+vue+element实现前后端分离校园失物招领系统详细设计
基于JavaSpringMVCvueelement实现前后端分离校园失物招领系统详细设计 博主介绍:多年java开发经验,专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 央顺技术团队 Java毕设项目精品实战案例《1000套》 欢迎点赞 收…...

【Stable Diffusion】入门-04:不同模型分类+代表作品+常用下载网站+使用技巧
目录 1 模型简介2 模型文件构成和加载位置2.1 存储位置2.2 加载模型 3 模型下载渠道3.1 HuggingFace3.2 Civitai 4 模型分类4.1 二次元模型4.2 写实模型4.3 2.5D模型 1 模型简介 拿图片给模型训练的这个过程,通常被叫做“喂图”。模型学习的内容不仅包括对具体事物…...

vue3之带参数的动态路由
在应用中,可以使用<router-link> 内置组件或 $router.push 方法来导航到带参数的路由。 定义路由 // 引入 Vue 和 Vue Router import { createRouter, createWebHistory } from vue-router; // 引入组件 import Home from ../views/Home.vue; import …...

深入探讨GPT系列与其他NLP架构的流行度差异及其应用解析
Transformer问答-1 为什么现在GPT系列的decoder-only那么流行,而其它两者:encoder-only和encoder-decoder架构不流行了呢? GPT系列(特别是从GPT-3开始)的流行并不意味着encoder-only或encoder-decoder架构不再流行或不再重要。事实上&…...

实现兼容性良好的前端页面开发
🤍 前端开发工程师、技术日更博主、已过CET6 🍨 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 🕠 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 🍚 蓝桥云课签约作者、上架课程《Vue.js 和 E…...

Rust学习02:推荐一本入门书,免费的
都说Rust的学习曲线很陡峭,试过才知雀实不容易。 先说我的基础,非科班,自学Python,写过几个小程序。 我买书从来不扣扣嗖嗖的,所以先啃了几本Rust的入门书,包括: Tim McNamara的《Rust实战》&am…...

npm run dev命令的执行顺序和原理
当我们在开发vue、react等项目的时候经常会用npm run *命令,那么当我们执行这个命令的时候具体都做了些什么呢?接下来我们就来详细探索一下 当执行npm run dev命令时,npm会按照以下步骤进行操作: 1. 查找并执行脚本: …...

C# SM2加解密 ——国密SM2算法
SM2 是国家密码管理局组织制定并提出的椭圆曲线密码算法标准。 本文使用第三方密码库 BouncyCastle 实现 SM2 加解密,使用 NuGet 安装即可,包名:Portable.BouncyCastle,目前最新版本为:1.9.0。 using Org.BouncyCastl…...

【Machine Learning】Suitable Learning Rate in Machine Learning
一、The cases of different learning rates: In the gradient descent algorithm model: is the learning rate of the demand, how to determine the learning rate, and what impact does it have if it is too large or too small? We will analyze it through the follow…...

力扣每日一题 矩阵中移动的最大次数 DP
Problem: 2684. 矩阵中移动的最大次数 复杂度 ⏰ 时间复杂度: O ( n m ) O(nm) O(nm) 🌎 空间复杂度: O ( n m ) O(nm) O(nm) Code class Solution { public int maxMoves(int[][] grid){int n grid.length;int m grid[0].length;int[][] f new int[n][m]…...

计算机网络 |内网穿透
其实内网穿透,也挺好玩的,如果在大学的时候,那个时候讲计算机网络的老师能横向延展,估计课也会更有趣不少,本来计算机网络这门课就是计算机课程中可玩性最搞的。 只能说,怪可惜的 回到正题,内网…...

爬虫学习 Scrapy中间件代理UA随机selenium使用
目录 中间件UA、代理处理---process_requestUA随机 代理处理seleniumscrapy 中间件 控制台操作 (百度只起个名 scrapy startproject mid scrapy genspider baidu baidu.com setting.py内 ROBOTSTXT_OBEY FalseLOG_LEVEL "WARNING"运行 scrapy crawl baidu middle…...

React理念——Fiber架构的主要原理
React理念——Fiber架构的主要原理 React 理念CPU 的瓶颈IO 的瓶颈 Fiber的产生及原理如何构建副作用链表 React 理念 从官网看到React的理念: React 是用 JavaScript 构建快速响应的大型 Web 应用程序的首选方式。它在 Facebook 和 Instagram 上表现优秀。 可见&a…...

[蓝桥杯练习题]确定字符串是否包含唯一字符/确定字符串是否是另一个的排列
确定字符串是否包含唯一字符 #include<bits/stdc.h> using namespace std; int main(){ios::sync_with_stdio(0);cin.tie(nullptr);cout.tie(nullptr);map<char,int>m;string s;cin>>s;for(int i0;i<s.size();i){if(isalpha(s[i]))s[i]tolower(s[i]);if(…...

鸿蒙Harmony应用开发—ArkTS声明式开发(容器组件:UIExtensionComponent (系统接口))
UIExtensionComponent用于支持在本页面内嵌入其他应用提供的UI。展示的内容在另外一个进程中运行,本应用并不参与其中的布局和渲染。 通常用于有进程隔离诉求的模块化开发场景。 说明: 该组件从API Version 10开始支持。后续版本如有新增内容࿰…...

Jenkins: 配合docker来部署项目
jenkins docker 部署 1 )测试将jenkins构建后的项目部署到docker的nginx镜像中 nginx 镜像内的默认目录在 /usr/share/nginx/html将待部署项目存放在 /usr/share/nginx/html 项目名称目录在Mac环境下的 jenkins系统 中,工程项目默认的路径在 ~/.jenkin…...

Leetcode 22. 括号生成
心路历程: 一开始看到左右括号,第一想到了栈。后来发现题目要求遍历所有的可能组合,第一想法是暴力for循环,但是不知道用几个for循环,所以想到递归和回溯。 虽然叫‘括号组合’,但是实际上这是一个满足规则…...

ChatGPT编程—实现小工具软件(批量替换文本、批量处理图像文件)
ChatGPT编程—实现小工具软件(批量替换文本、批量处理图像文件) 今天借助[小蜜蜂AI][https://zglg.work]网站的ChatGPT编程实现一个功能:批量处理文件及其内容,例如批量替换文本、批量处理图像文件等。 环境:Pycharm 2021 系统:…...