37 | 实战二(下):重构ID生成器项目中各函数的异常处理代码

平时进行软件设计开发的时候,我们除了要保证正常情况下的逻辑运行正确之外,还需要编写大量额外的代码,来处理有可能出现的异常情况,以保证代码在任何情况下,都在我们的掌控之内,不会出现非预期的运行结果。程序的 bug 往往都出现在一些边界条件和异常情况下,所以说,异常处理得好坏直接影响了代码的健壮性。全面、合理地处理各种异常能有效减少代码 bug,也是保证代码质量的一个重要手段。

在上一篇文章中,我们讲解了几种异常情况的处理方式,比如返回错误码、NULL 值、空对象、异常对象。针对最常用的异常对象,我们还重点讲解了两种异常类型的应用场景,以及针对函数抛出的异常的三种处理方式:直接吞掉、原封不动地抛出和包裹成新的异常抛出。

除此之外,在上一篇文章的开头,我们还针对 ID 生成器的代码,提出了 4 个有关异常处理的问题。今天,我们就用一篇文章的时间,结合上一篇文章讲到的理论知识,来逐一解答一下这几个问题。

话不多说,让我们正式开始今天的内容吧!

重构 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-16723733647-83Ab3uK6”这样的数据。如果主机名获取失败,substrOfHostName 为空字符串,那 generate() 函数会返回类似“-16723733647-83Ab3uK6”这样的数据。

在异常情况下,返回上面两种特殊的 ID 数据格式,这样的做法是否合理呢?这个其实很难讲,我们要看具体的业务是怎么设计的。不过,我更倾向于明确地将异常告知调用者。所以,这里最好是抛出受检异常,而非特殊值。

按照这个设计思路,我们对 generate() 函数进行重构。重构之后的代码如下所示:

  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() 函数

对于 getLastFieldOfHostName() 函数,是否应该将 UnknownHostException 异常在函数内部吞掉(try-catch 并打印日志),还是应该将异常继续往上抛出?如果往上抛出的话,是直接把 UnknownHostException 异常原封不动地抛出,还是封装成新的异常抛出?

  private String getLastFieldOfHostName() {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;}

现在的处理方式是当主机名获取失败的时候,getLastFieldOfHostName() 函数返回 NULL 值。我们前面讲过,是返回 NULL 值还是异常对象,要看获取不到数据是正常行为,还是异常行为。获取主机名失败会影响后续逻辑的处理,并不是我们期望的,所以,它是一种异常行为。这里最好是抛出异常,而非返回 NULL 值。

至于是直接将 UnknownHostException 抛出,还是重新封装成新的异常抛出,要看函数跟异常是否有业务相关性。getLastFieldOfHostName() 函数用来获取主机名的最后一个字段,UnknownHostException 异常表示主机名获取失败,两者算是业务相关,所以可以直接将 UnknownHostException 抛出,不需要重新包裹成新的异常。

按照上面的设计思路,我们对 getLastFieldOfHostName() 函数进行重构。重构后的代码如下所示:

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

getLastFieldOfHostName() 函数修改之后,generate() 函数也要做相应的修改。我们需要在 generate() 函数中,捕获 getLastFieldOfHostName() 抛出的 UnknownHostException 异常。当我们捕获到这个异常之后,应该怎么处理呢?

按照之前的分析,ID 生成失败的时候,我们需要明确地告知调用者。所以,我们不能在 generate() 函数中,将 UnknownHostException 这个异常吞掉。那我们应该原封不动地抛出,还是封装成新的异常抛出呢?

我们选择后者。在 generate() 函数中,我们需要捕获 UnknownHostException 异常,并重新包裹成新的异常 IdGenerationFailureException 往上抛出。之所以这么做,有下面三个原因。

  • 调用者在使用 generate() 函数的时候,只需要知道它生成的是随机唯一 ID,并不关心 ID 是如何生成的。也就说是,这是依赖抽象而非实现编程。如果 generate() 函数直接抛出 UnknownHostException 异常,实际上是暴露了实现细节。
  • 从代码封装的角度来讲,我们不希望将 UnknownHostException 这个比较底层的异常,暴露给更上层的代码,也就是调用 generate() 函数的代码。而且,调用者拿到这个异常的时候,并不能理解这个异常到底代表了什么,也不知道该如何处理。
  • UnknownHostException 异常跟 generate() 函数,在业务概念上没有相关性。

按照上面的设计思路,我们对 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() 函数

对于 getLastSubstrSplittedByDot(String hostName) 函数,如果 hostName 为 NULL 或者空字符串,这个函数应该返回什么?

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

理论上讲,参数传递的正确性应该有程序员来保证,我们无需做 NULL 值或者空字符串的判断和特殊处理。调用者本不应该把 NULL 值或者空字符串传递给 getLastSubstrSplittedByDot() 函数。如果传递了,那就是 code bug,需要修复。但是,话说回来,谁也保证不了程序员就一定不会传递 NULL 值或者空字符串。那我们到底该不该做 NULL 值或空字符串的判断呢?

如果函数是 private 类私有的,只在类内部被调用,完全在你自己的掌控之下,自己保证在调用这个 private 函数的时候,不要传递 NULL 值或空字符串就可以了。所以,我们可以不在 private 函数中做 NULL 值或空字符串的判断。如果函数是 public 的,你无法掌控会被谁调用以及如何调用(有可能某个同事一时疏忽,传递进了 NULL 值,这种情况也是存在的),为了尽可能提高代码的健壮性,我们最好是在 public 函数中做 NULL 值或空字符串的判断。

那你可能会说,getLastSubstrSplittedByDot() 是 protected 的,既不是 private 函数,也不是 public 函数,那要不要做 NULL 值或空字符串的判断呢?

之所以将它设置为 protected,是为了方便写单元测试。不过,单元测试可能要测试一些 corner case,比如输入是 NULL 值或者空字符串的情况。所以,这里我们最好也加上 NULL 值或空字符串的判断逻辑。虽然加上有些冗余,但多加些检验总归不会错的。

按照这个设计思路,我们对 getLastSubstrSplittedByDot() 函数进行重构。重构之后的代码如下所示:

  @VisibleForTestingprotected 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() 函数

对于 generateRandomAlphameric(int length) 函数,如果 length < 0 或 length = 0,这个函数应该返回什么?

  @VisibleForTestingprotected 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 的情况。生成一个长度为负值的随机字符串是不符合常规逻辑的,是一种异常行为。所以,当传入的参数 length < 0 的时候,我们抛出 IllegalArgumentException 异常。

我们再来看 length = 0 的情况。length = 0 是否是异常行为呢?这就看你自己怎么定义了。我们既可以把它定义为一种异常行为,抛出 IllegalArgumentException 异常,也可以把它定义为一种正常行为,让函数在入参 length = 0 的情况下,直接返回空字符串。不管选择哪种处理方式,最关键的一点是,要在函数注释中,明确告知 length = 0 的情况下,会返回什么样的数据。

重构之后的 RandomIdGenerator 代码

对 RandomIdGenerator 类中各个函数异常情况处理代码的重构,到此就结束了。为了方便查看,我把重构之后的代码,重新整理之后贴在这里了。你可以对比着看一下,跟你的重构思路是否一致。

public class RandomIdGenerator implements IdGenerator {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("...", 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;}@VisibleForTestingprotected String getLastSubstrSplittedByDot(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 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);}
}

重点回顾

好了,今天的内容到此就讲完了。我们一块来总结回顾一下,你需要重点掌握的内容。

今天的内容比较偏实战,是对上篇文章学到的理论知识的一个应用。从今天的实战中,你学到了哪些更高层的软件设计和开发思想呢?我这里抛砖引玉,总结了下面 3 点。

  • 再简单的代码,看上去再完美的代码,只要我们下功夫去推敲,总有可以优化的空间,就看你愿不愿把事情做到极致。
  • 如果你内功不够深厚,理论知识不够扎实,那你就很难参透开源项目的代码到底优秀在哪里。就像如果我们没有之前的理论学习,没有今天我给你一点一点重构、讲解、分析,只是给你最后重构好的 RandomIdGenerator 的代码,你真的能学到它的设计精髓吗?
  • 对比第 34 篇文章最初我的 IdGenerator 代码和最终的 RandomIdGenerator 代码,它们一个是“能用”,一个是“好用”,天壤之别。作为一名程序员,起码对代码要有追求啊,不然跟咸鱼有啥区别!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/55633.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Mysql(六) --- 聚合函数,分组和联合查询

文章目录 前言1.聚合函数1.1.常用的函数1.2.COUNT()1.3.SUM()1.4.AVG()1.5.MIN()、MAX() 2.GROUP BY 分组查询2.1.语法2.2.示例2.3.HAVING 子句 3.联合查询3.1.为什么要进行联合查询3.2.那么是如何进行联合查询的3.3.示例&#xff1a;一个完整的联合查询的过程3.4.内连接3.5.外…

Python知识点:如何使用Airflow进行ETL任务调度

开篇&#xff0c;先说一个好消息&#xff0c;截止到2025年1月1日前&#xff0c;翻到文末找到我&#xff0c;赠送定制版的开题报告和任务书&#xff0c;先到先得&#xff01;过期不候&#xff01; 要使用Apache Airflow进行ETL任务调度&#xff0c;你可以遵循以下步骤&#xff1…

C++(异常)

目录 C语言传统的处理错误的方式 传统的错误处理机制 C异常概念 异常的使用 异常的抛出和捕获 异常的抛出和匹配原则 在函数调用链中异常栈展开匹配原则 异常的重新抛出 异常安全 异常规范 自定义异常体系 C标准库的异常体系 异常的优缺点 C异常的优点 C异常的缺…

「自动化测试」Selenium 的使用

使用 Selenium 需要先导入相关依赖 <dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-java</artifactId> <version>4.0.0</version> </dependency><dependency><groupId>io.gith…

【M365运维】在SPO文档库里删除文档时,遇到文档被签出无法删除。

【问题】SPO的存储空间剩的不多了&#xff0c;在清理文档库时&#xff0c;遇到有些文档被签出但用户已经离职&#xff0c;删除文件时报错。 【解决】翻SPO的设置时&#xff0c;看到有“管理没有已签入版本的文件”&#xff0c;在里面获取文件的所有权之后就可以删除了。 具体…

API 数据接口:使用操作流程与安全指南

在当今数字化高速发展的时代&#xff0c;API 数据接口如同构建数字世界的关键纽带&#xff0c;将不同的软件系统和服务紧密连接在一起。无论是企业开发者致力于提升业务效率&#xff0c;还是个人用户追求更便捷的数字体验&#xff0c;深入了解 API 数据接口的使用操作流程以及全…

【树莓派5B】IO串口通信使用

超级简单的串口使用 前言零、检查准备&#xff08;可略&#xff09;0.1 查看UART引脚&#xff1a;0.2 扩展一下引脚查看的方法 一、配置准备1.1 检查端口配置1.2 查看串口映射1.3 下载minicom串口调试工具1.4 通过命令获取串口上的数据 二、python的serial进行收发测试总结 前言…

sqli-labs靶场第二关less-2

sqli-labs靶场第二关less-2 本次测试在虚拟机搭建靶场&#xff0c;从主机测试 1、输入?id1和?id2发现有不同的页面回显 2、判断注入类型 http://192.168.128.3/sq/Less-2/?id1’ 从回显判断多一个‘ &#xff0c;预测可能是数字型注入 输入 http://192.168.128.3/sq/Less…

Study-Oracle-10-ORALCE19C-RAC集群维护

一路走来,所有遇到的人,帮助过我的、伤害过我的都是朋友,没有一个是敌人。 一、RAC的逻辑架构与进程 1、RAC 与单实例进程的对比 2、RAC相关进程功能 3、在主机查看RAC后台进程 快捷键设置 alias sqlplus=rlwrap sqlplus alias rman=rlwrap rman alias crsctl=/u01/app…

使用springCache实现缓存

简介 这个springCache貌似jdk8或者以上才可以 cache最好加在controller层&#xff0c;毕竟返回给前端的数据&#xff0c;在这一步才是最完整的&#xff0c;缓存controller的数据才有意义 配置 导入依赖 <dependency><groupId>org.springframework.boot</groupId…

go语言 常用的web框架

go语言 常用的web框架 1. Gin2. Echo3. Beego4.GoFrame Go语言有许多流行的web框架&#xff0c;以下是其中几个&#xff1a; 1. Gin Gin是一个高性能的HTTP web框架&#xff0c;具有简洁的API和快速的路由引擎。它也有许多中间件和插件&#xff0c;方便开发者进行功能扩展。代…

基于Python的美术馆预约系统【附源码】

效果如下&#xff1a; 系统首页界面 系统注册页面 美术馆详细页面 公告信息详细页面 后台登录界面 管理员主界面 美术馆管理界面 预约参观管理界面 研究背景 随着文化娱乐活动的日益丰富&#xff0c;美术馆作为展示艺术作品、传播文化的重要场所&#xff0c;其管理和服务模式…

字段临时缓存包装器

前言 在实际开发中&#xff0c;我们有时候存在一种需求&#xff0c;例如对于某个字段&#xff0c;我们希望在某个明确的保存节点前对字段的修改都仅作为缓存保留&#xff0c;最终是否应用这些修改取决于某些条件&#xff0c;比如玩家对游戏设置的修改可能需要玩家明确确认应用修…

代码随想录 102. 沉没孤岛

102. 沉没孤岛 #include<bits/stdc.h> using namespace std;void dfs(vector<vector<int>>& mp, vector<vector<int>>& visit, int y, int x){if (mp[y][x] 0 || visit[y][x] 1) return;if (mp[y][x] 1 && visit[y][x] 0) …

go语言protoc的详细用法与例子

一. 原来的项目结构 二. 选择源proto文件及其目录&目的proto文件及其目录 在E:\code\go_test\simple_demo\api 文件夹下&#xff0c;递归创建\snapshot\helloworld\v1\ad.pb.go E:\code\go_test\simple_demo> protoc --go_outpathssource_relative:./api .\snapshot\h…

[OS] EXPORT_SYMBOL()

在 Linux 内核中&#xff0c;EXPORT_SYMBOL() 用于将模块中的函数或变量导出&#xff0c;使得其他内核模块能够使用这些导出的符号。这对于模块之间共享功能或数据非常有用。给出的代码示例展示了如何使用 EXPORT_SYMBOL() 将变量和函数导出供其他模块使用。 /* ... */ int GL…

Dolma:包含三万亿Token的语言模型预训练研究开放语料库

前言 原论文&#xff1a;Dolma: an Open Corpus of Three Trillion Tokens for Language Model Pretraining Research 摘要 关于训练当前最佳性能语言模型的预训练语料库的信息很少被讨论——商业模型很少详细说明它们的数据&#xff0c;即使是开源模型也往往在没有训练数据…

Ubuntu开机进入紧急模式处理

文章目录 Ubuntu开机进入紧急模式处理一、问题描述二、解决办法参考 Ubuntu开机进入紧急模式处理 一、问题描述 Ubuntu开机不能够正常启动&#xff0c;自动进入紧急模式&#xff08;You are in emergency mode&#xff09;。具体如下所示&#xff1a; 二、解决办法 按CtrlD进…

基于开源大型lmm模型生成标签对InternVL2-1B等轻量lmm模型进行微调

基于开源大型lmm模型生成标签对InternVL2-1B等轻量lmm模型进行微调,提升InternVL2-1B等轻量lmm模型的能力。本实验在window下,基于3060 12g显卡进行实验。基于qwen2-vl 7b模型生成标签(电脑显存大的话可以考虑qwen2-vl 72b模型),然后对InternVL2-1B进行Lora微调。以voc201…

Perl 子程序(函数)

Perl 子程序&#xff08;函数&#xff09; Perl 是一种高级、解释型、动态编程语言&#xff0c;广泛用于CGI脚本、系统管理、网络编程、 finance, bioinformatics, 以及其他领域。在Perl中&#xff0c;子程序&#xff08;也称为函数&#xff09;是组织代码和重用代码块的重要方…