37 | 实战二(下):重构ID生成器项目中各函数的异常处理代码
下载APP
关闭
渠道合作
推荐作者
37 | 实战二(下):重构ID生成器项目中各函数的异常处理代码
2020-01-27 王争 来自北京
《设计模式之美》
课程介绍
讲述:冯永吉
时长10:50大小9.92M
平时进行软件设计开发的时候,我们除了要保证正常情况下的逻辑运行正确之外,还需要编写大量额外的代码,来处理有可能出现的异常情况,以保证代码在任何情况下,都在我们的掌控之内,不会出现非预期的运行结果。程序的 bug 往往都出现在一些边界条件和异常情况下,所以说,异常处理得好坏直接影响了代码的健壮性。全面、合理地处理各种异常能有效减少代码 bug,也是保证代码质量的一个重要手段。
在上一节课中,我们讲解了几种异常情况的处理方式,比如返回错误码、NULL 值、空对象、异常对象。针对最常用的异常对象,我们还重点讲解了两种异常类型的应用场景,以及针对函数抛出的异常的三种处理方式:直接吞掉、原封不动地抛出和包裹成新的异常抛出。
除此之外,在上一节课的开头,我们还针对 ID 生成器的代码,提出了 4 个有关异常处理的问题。今天,我们就用一节课的时间,结合上一节课讲到的理论知识,来逐一解答一下这几个问题。
话不多说,让我们正式开始今天的内容吧!
重构 generate() 函数
首先,我们来看,对于 generate() 函数,如果本机名获取失败,函数返回什么?这样的返回值是否合理?
ID 由三部分构成:本机名、时间戳和随机数。时间戳和随机数的生成函数不会出错,唯独主机名有可能获取失败。在目前的代码实现中,如果主机名获取失败,substrOfHostName 为 NULL,那 generate() 函数会返回类似“null-16723733647-83Ab3uK6”这样的数据。如果主机名获取失败,substrOfHostName 为空字符串,那 generate() 函数会返回类似“-16723733647-83Ab3uK6”这样的数据。
在异常情况下,返回上面两种特殊的 ID 数据格式,这样的做法是否合理呢?这个其实很难讲,我们要看具体的业务是怎么设计的。不过,我更倾向于明确地将异常告知调用者。所以,这里最好是抛出受检异常,而非特殊值。
按照这个设计思路,我们对 generate() 函数进行重构。重构之后的代码如下所示:
重构 getLastFieldOfHostName() 函数
对于 getLastFieldOfHostName() 函数,是否应该将 UnknownHostException 异常在函数内部吞掉(try-catch 并打印日志),还是应该将异常继续往上抛出?如果往上抛出的话,是直接把 UnknownHostException 异常原封不动地抛出,还是封装成新的异常抛出?
现在的处理方式是当主机名获取失败的时候,getLastFieldOfHostName() 函数返回 NULL 值。我们前面讲过,是返回 NULL 值还是异常对象,要看获取不到数据是正常行为,还是异常行为。获取主机名失败会影响后续逻辑的处理,并不是我们期望的,所以,它是一种异常行为。这里最好是抛出异常,而非返回 NULL 值。
至于是直接将 UnknownHostException 抛出,还是重新封装成新的异常抛出,要看函数跟异常是否有业务相关性。getLastFieldOfHostName() 函数用来获取主机名的最后一个字段,UnknownHostException 异常表示主机名获取失败,两者算是业务相关,所以可以直接将 UnknownHostException 抛出,不需要重新包裹成新的异常。
按照上面的设计思路,我们对 getLastFieldOfHostName() 函数进行重构。重构后的代码如下所示:
getLastFieldOfHostName() 函数修改之后,generate() 函数也要做相应的修改。我们需要在 generate() 函数中,捕获 getLastFieldOfHostName() 抛出的 UnknownHostException 异常。当我们捕获到这个异常之后,应该怎么处理呢?
按照之前的分析,ID 生成失败的时候,我们需要明确地告知调用者。所以,我们不能在 generate() 函数中,将 UnknownHostException 这个异常吞掉。那我们应该原封不动地抛出,还是封装成新的异常抛出呢?
我们选择后者。在 generate() 函数中,我们需要捕获 UnknownHostException 异常,并重新包裹成新的异常 IdGenerationFailureException 往上抛出。之所以这么做,有下面三个原因。
调用者在使用 generate() 函数的时候,只需要知道它生成的是随机唯一 ID,并不关心 ID 是如何生成的。也就说是,这是依赖抽象而非实现编程。如果 generate() 函数直接抛出 UnknownHostException 异常,实际上是暴露了实现细节。
从代码封装的角度来讲,我们不希望将 UnknownHostException 这个比较底层的异常,暴露给更上层的代码,也就是调用 generate() 函数的代码。而且,调用者拿到这个异常的时候,并不能理解这个异常到底代表了什么,也不知道该如何处理。
UnknownHostException 异常跟 generate() 函数,在业务概念上没有相关性。
按照上面的设计思路,我们对 generate() 的函数再次进行重构。重构后的代码如下所示:
重构 getLastSubstrSplittedByDot() 函数
对于 getLastSubstrSplittedByDot(String hostName) 函数,如果 hostName 为 NULL 或者空字符串,这个函数应该返回什么?
理论上讲,参数传递的正确性应该有程序员来保证,我们无需做 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() 函数进行重构。重构之后的代码如下所示:
按照上面讲的,我们在使用这个函数的时候,自己也要保证不传递 NULL 值或者空字符串进去。所以,getLastFieldOfHostName() 函数的代码也要作相应的修改。修改之后的代码如下所示:
重构 generateRandomAlphameric() 函数
对于 generateRandomAlphameric(int length) 函数,如果 length < 0 或 length = 0,这个函数应该返回什么?
我们先来看 length < 0 的情况。生成一个长度为负值的随机字符串是不符合常规逻辑的,是一种异常行为。所以,当传入的参数 length < 0 的时候,我们抛出 IllegalArgumentException 异常。
我们再来看 length = 0 的情况。length = 0 是否是异常行为呢?这就看你自己怎么定义了。我们既可以把它定义为一种异常行为,抛出 IllegalArgumentException 异常,也可以把它定义为一种正常行为,让函数在入参 length = 0 的情况下,直接返回空字符串。不管选择哪种处理方式,最关键的一点是,要在函数注释中,明确告知 length = 0 的情况下,会返回什么样的数据。
重构之后的 RandomIdGenerator 代码
对 RandomIdGenerator 类中各个函数异常情况处理代码的重构,到此就结束了。为了方便查看,我把重构之后的代码,重新整理之后贴在这里了。你可以对比着看一下,跟你的重构思路是否一致。
重点回顾
好了,今天的内容到此就讲完了。我们一块来总结回顾一下,你需要重点掌握的内容。
今天的内容比较偏实战,是对上节课学到的理论知识的一个应用。从今天的实战中,你学到了哪些更高层的软件设计和开发思想呢?我这里抛砖引玉,总结了下面 3 点。
再简单的代码,看上去再完美的代码,只要我们下功夫去推敲,总有可以优化的空间,就看你愿不愿把事情做到极致。
如果你内功不够深厚,理论知识不够扎实,那你就很难参透开源项目的代码到底优秀在哪里。就像如果我们没有之前的理论学习,没有今天我给你一点一点重构、讲解、分析,只是给你最后重构好的 RandomIdGenerator 的代码,你真的能学到它的设计精髓吗?
对比第 34 节课最初小王的 IdGenerator 代码和最终的 RandomIdGenerator 代码,它们一个是“能用”,一个是“好用”,天壤之别。作为一名程序员,起码对代码要有追求啊,不然跟咸鱼有啥区别!
课堂讨论
我们花了 4 节课的时间,对一个非常简单的、不到 40 行的 ID 生成器代码,做了多次迭代重构。除了刚刚我在“重点回顾”中讲到的那几点之外,从这个迭代重构的过程中,你还学到哪些更有价值的东西?
欢迎在留言区写下你的思考和想法,和同学一起交流和分享。如果有收获,也欢迎你把这篇文章分享给你的朋友。
分享给需要的人,Ta购买本课程,你将得29元
生成海报并分享
赞 70
提建议
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
上一篇
36 | 实战二(上):程序出错该返回啥?NULL、异常、错误码、空对象?
下一篇
38 | 总结回顾面向对象、设计原则、编程规范、重构技巧等知识点
精选留言(85)
- Jxin2020-01-27还学到什么: 1.一下子想搞个例子讲这些真的太难了,拍着脑子想demo。栏主这个demo背景简单,也将要讲的内容串起来了,实属不易,幸苦栏主了。 个人见解: 1.按我的习惯,我会尽量把入参和中间不可靠变量的异常校验都放在public方法,所有私有方法都以契约的方式不再做参数校验。也就是说 public方法干 1.参数校验 2. 系统一级流程编排 3.统一异常处理 这三件事。所以对private方法的提炼会和栏主有点出入。 2.如果这个id生成器还要带有业务key,比如分表路由key之类的东西。那么这个实现就还得大动干戈。但凡这种涉及持久数据的玩意,很可能需要考虑新老版本兼容的问题,也就是如何平滑过度老数据。所以需要在id生成算法上引入版本或者类型的标记,把标记打在持久化的数据上,以备平滑过度老数据。展开
作者回复: 我觉得你懂我~
共 13 条评论201 - 空白昵称2020-03-13我觉得抛异常这件事,有点像开发经理的职责。下级有问题(异常)要反馈,然后自己能处理则处理(吞掉异常)。如果自己不能处理的,要向上级汇报,那么汇报的时候就要考虑,如果上级不关心底层研发问题,则上报自己汇总的问题(re-throw新的异常)。如果上级领导也是技术控,对底层很了解,那么可适当直接上报(直接re-throw下级反馈的异常)。共 6 条评论155
- undefined2020-02-01个人见解:如果 id 生成器需要应用到生产环境,类似 hostname 获取失败的问题,需要由生成器本身给出降级方案。一来为了 id 格式统一,二来假若抛给业务,业务对于这种系统底层的失败,也没有什么好的解决方法。共 8 条评论57
- Harvey2020-01-27设计之所以难是因为没有标准答案,很多权衡是依赖于具体业务的。这就是DDD的思想所在,要先想清楚问题域是什么在思考解决方案。很多开发讨论问题的时候没有层次,上来就陷入技术细节,这就叫缺乏抽象。下游系统要想清楚哪些是上游系统给你提供的服务?哪些是人家的内部技术实现?比如ID生成,作为上游系统,ID生成服务提供的是有小概率重复的随机ID服务,至于随机算法,下游系统不必关心,这是上游系统的内部实现,这样上游系统才有空间更换算法而不影响下游系统。展开共 3 条评论24
- Geek_kobe2020-01-27果然还是看技术文章能让恐慌的心静下来共 1 条评论25
- xk_2020-03-16很多小伙伴说,generate方法不需要抛出异常。 对,如果这个业务是仅仅只是作为唯一id,那么不管怎么样一定要生成一个随机数的。 那么,放开来想这个业务并非是一个id生成器,而是生成一个id业务码,这个id后面会用个主机名,对流量进行识别。争哥这么抛出异常,我觉得是可行的。还是要看具体的业务 再说一句,主机名还是一个很重要的东西,hadoop集群获取不到主机名,是一个很严重的问题。展开共 1 条评论13
- 皮卡皮卡2020-01-28争哥这种设计思路考虑了一下,但是在业务中往往获取唯一ID的地方,不关心ID内部生成错误,需要的只是能够返回出来ID即可。目前我们的处理是异常在generate内部自己解决,同时返回ID共 4 条评论12
- 高源2020-01-27希望老师每节课举的代码有下载的地方,自己下载下来结合老师讲解的,自己理解体会其中的解决问题
作者回复: 好的,等我俩月,我整理好,一块放到github上: https://github.com/wangzheng0822
共 4 条评论10 - 斐波那契2020-02-03从来没有否认过争哥这个专栏的认真程度,但是对于generate方法是否抛出异常有点异议 我的想法跟下面的人是一样的 本质上这是个id生成器 是为了追踪请求错误时候用的 在这个条件下id能不能生成并不应该阻止请求的流程 假如抛出异常给调用者那调用者继续走下去 那这个抛出来的异常的价值在哪?就只是为了知道一下hostname获取不到?如果抛出异常后终止了请求 那会不会有点”小题大作“了?当然demo怎么样举都可能有不完美的地方 评论里说出来也是给其他读者一个思路而不是一味的“照搬” 而且我觉得这个专栏争哥举了那么多的demo的牛逼之处在于不仅把要讲的知识点抛砖引玉出来而且还是贴近我们的日常开发 确实是实实在在很有可能在企业里用到的案例 就比如今天这个demo 后面我就考虑在我新开发的接口添加id生成器 来追踪请求出现的问题 说实话 我并没有看过争哥的算法课程 但是看到争哥这个专栏的前言后毫不犹豫地订了 追求代码极致这一态度是争哥给我的共鸣展开共 4 条评论7
- 李小四2020-02-07设计模式_37: 刚刚看了一下,这4篇文章,我做了14条笔记,这些东西都是我认为非常好的细节。 随便举一个例子: ``` 使用注解 @VisibleForTesting 来表示某private方法改为protected只是为了便于单元测试。 ``` 很喜欢这样的细节,当时的感受是,这种规范的做法是非常好的习惯,读起来非常友好。 剩下的也都差不多,我自己的开发中是注意不到的,缺乏这样的智慧。 另外,我也非常同意 @Jxin 同学的说法,找到一个Demo,能够涵盖绝大多数的要点,同时例子不能很生僻,并且让别人容易看懂。我也经常写文章,我知道这里的困哪和工作量。 佩服争哥!展开5
- liu_liu2020-01-28写代码不是糊弄,写出好的有水平的代码需要下一番功夫。对代码保持敬畏之心,有追求极致的思想,才会越来越好。共 1 条评论5
- Promise°2020-08-10学到了什么: 脑袋大,看了评论感觉:公说公有理婆说婆有理。同意@Jxin的说法 私有方法以契约的形式保证参数不会出错。 如果该生成器仅为此项目内部使用或者以工具类的形式存在不建议generate方法抛出异常。 比如所有的请求都需要增加logId,那么如果获取不到hostName就抛出异常且调用方不捕获异常的场景下,所有的请求都会失败。 如果是5台机器的集群组那么就相当于挂掉了1台。个人认为影响还是很大的。 但是反过来说对于这种场景 也并非无解决方案,比如项目启动时必须先调用id生成器一次,保证id必定能生成,否则抛出异常终止启动。 (仅指集群应用): 1.项目部署就能发现问题,避免上线后再产生问题,把问题扼杀在启动时。 2.集群应用下一般来说都不会一次性部署所有机器。原则上每次并行部署的项目数量要小于机器数/2。 比如十台机器可能先部署2台 启动无误后在按照2台的数量依次部署。这样即使是由于环境产生了问题 也就只有两台机器出现问题,不会导致全部不可用 以上纯属个人见解,欢迎大家留言讨论。如有错误之处 也请王争老师和大家多多指点。展开共 3 条评论4
- your problem?2020-01-27打卡,也祝大家新年快乐,身体健康,另外我始终觉得generateRandomAlphameric这个函数里,随机获取这个写法很不利于性能测试,假如这个函数会被百万,甚至千万次的调用,不可控性也太强了,我觉得可以改成随机生成0-26的数字,对应去加到字母的位置,不知道老师和大家有什么想法吗共 1 条评论4
- Heaven2020-03-04是的,这个代码写的足够健壮性,而且相当完美了,但是我接触的实际开发过程中,其实很多时候,上层系统并不关系ID内部生成是否错误,也不希望因为ID生成错误而导致系统崩溃,于是,一般情况下,我们还是将ID生成器的异常不向外抛出,只是再返回的时候加上错误码,回来查看日志的时候,根据日志时间找到对应异常,再进行分析原因共 1 条评论3
- xuanyuan2020-02-12很多实战的理念我都没有在书上看过,但是想法出奇一致。越来越感觉能和您一起工作,真是不要太幸福3
- 迷羊2020-01-28还学到了: 1.函数出错时是返回NULL还是异常对象? 要看获取不到数据是正常行为,还是异常行为,如果业务上来说是异常行为就抛出异常,反之返回NULL。 2.是直接返回出错的异常还是重新封装成新的异常? 要看函数跟异常是否有业务相关性。相关的话就直接抛出。不相关就包装成与函数相关的异常类型,而且这样也能隐藏实现细节。 3.NULL值或空字符串在什么时候需要判断? a.如果函数是 private 类私有的,只在类内部被调用,完全在你自己的掌控之下,自己保证在调用这个 private 函数的时候,不要传递 NULL 值或空字符串就可以了。 b.如果函数是 public 的,你无法掌控会被谁调用以及如何调用(有可能某个同事一时疏忽,传递进了 NULL 值,这种情况也是存在的),为了尽可能提高代码的健壮性,我们最好是在 public 函数中做 NULL 值或空字符串的判断。 c.但是单元测试会测试一些corner case,所以,最好也加上判断。 4.个人的一点思考 如果代码中报的错是受检异常就可以针对具体情况来处理是throws出去、吞掉还是包装新的异常。如果报的错是非受检异常我还是习惯内部自己处理,因为非受检异常throws出去的话,调用方不处理,编译器也不会报错,所以,为了防止调用方未处理的情况,还是自己内部处理吧。展开共 1 条评论2
- Frank2020-01-27今天学习了异常代码处理思路。在处理到异常时,通常会将上层关心的异常直接包装成RuntimeException往上抛,没有根据业务域定义相关的自定义异常。通过今天的学习,了解到处理异常的基本思路:是往上抛还是吞掉,主要看调用者是够关心该异常。是否要包装成新的异常主要看调用者是否理解该异常,该异常是否业务相关。如果能理解、业务相关可以直接抛,否则重新包装。 在这4节课的持续迭代过程中,除了文章中提到的开发思想,自己总结了如下一些个人想法: 1. 科比说过“我现在所做的一切,都是为了追求更加完美” - 缅怀逝去的伟大的科比。我们对生活,工作都要尽量追求完美。 2. 人生是个不断重构自己的过程,自己写的代码也要不断持续重构,优化。这样自己才能不断进步。 3. 参考优秀的开发思想,方法论,不断地将之实践,总结,改进,逐渐形成合适自己的方法论。展开2
- Lingo2021-10-27一个程序员的良好修养 1: "总有刁民想害朕"的心态,public方法传参要检验。 2: 人格分裂,既要站在上层的方法想是否关心此异常,又要站在下层的方法想是否要抛出此异常。 3:高水平甩锅技巧,参数的问题,直接甩给上层让app崩溃,fail first。底层技术问题,要包装一下甩给上层,这个活我干不了。力所能及的问题,自己干了算了,打个warn log,发个免责声明。展开1
- 马建华2021-08-03太受震撼了,原来这么简单的代码都有如此深度的优化空间!1
- Jerry2021-04-01自从看了老师的专栏,自己都快走火入魔了,居然不会写代码了。Soga。。。1