王争《设计模式之美》学习笔记
第一轮重构:提高代码的可读性
- hostName 变量不应该被重复使用,尤其当这两次使用时的时候;
- 将获取 hostName 的代码抽离出来,定义为 getLastfieldOfHostName() 函数;
- 删除代码中的魔法数,比如,57、90、97、122;
- 将随机数生成的代码抽离出来,定义为 generateRandomAlphameric() 函数;
- generate() 函数中的三个 if 逻辑重复了,且实现过于复杂,我们要对其进行简化;
- 对 IdGenerator 类重命名,并且抽象出对应的接口。
对 IdGenerator 类重命名,并且抽象出对应的接口
接口:IdGenerator,实现类:LogTraceIdGenerator
- 如果我们扩展新的日志 ID 生成算法,也就是要创建另一个新的实现类,因为原来的实现类已经叫 LogTraceIdGenerator 了,命名过于通用,那新的实现类就不好取名了,无法取一个跟 LogTraceIdGenerator 平行得名字了。
- 假设我们没有日志 ID 的扩展需求,但要扩展其他业务的 ID 生成算法,比如针对用户的(UserldGenerator)、订单的(OrderIdGenerator),从命名上来看涉及的是完全不同的业务,不存在互相替换的场景,实现同一个接口,实际上是没有意义的。
- 结论,不好。
接口:LogTraceIdGenerator,实现类:HostNameMillisIdGenerator
- LogTraceIdGenerator 接口的命名是合理的。
- HostNameMillisIdGenerator 实现类暴露了太多实现细节,只要代码稍微有所改动,就可能需要改动命名,才能匹配实现。
- 结论,不好。
接口:LogTraceIdGenerator,实现类:RandomIdGenerator
- 在目前的 ID 生成器代码实现中,我们生成的 ID 是一个随机 ID,不是递增有序的,所以,命名成 RandomIdGenerator 是比较合理的,即便内部生成算法有所改动,只要生成的还是随机的 ID,就不需要改动命名。
- 如果我们需要扩展新的 ID 生成算法,比如要实现一个递增有序的 ID 生成算法,那我们可以命名为 SequenceIdGenerator。
- 结论,推荐。
更为推荐的方式
- 我们抽象出两个接口,一个是 IdGenerator,一个是 LogTraceIdGenerator,LogTraceIdGenerator 继承 IdGenerator。
- 实现类实现接口 IdGenerator,命名为 RandomIdGenerator、SequenceIdGenerator 等。
第二轮重构:提高代码的可测试性
generate() 函数定义为静态函数,会影响使用该函数的可测试性
- 在第一轮重构中,我们将 RandomIdGenerator 类中的 generate() 静态函数重新定义成了普通函数。
- 调用者可以通过依赖注入的方式,在外部创建好 RandomIdGenerator 对象后注入到自己的代码中,从而解决静态函数调用影响代码可测试性的问题。
generate() 函数的代码实现依赖运行环境(本机名)、时间函数、随机函数,所以 generate() 函数本身的可测试性也不好
- 从 getLastfieldOfHostName() 函数中,将逻辑比较复杂的那部分代码剥离出来,定义为 getLastSubstrSplittedByDot() 函数。因为 getLastfieldOfHostName() 函数依赖本地主机名,所以,剥离出主要代码之后这个函数变得非常简单,可以不用测试。我们重点测试 getLastSubstrSplittedByDot() 函数即可。
- 将 generateRandomAlphameric() 和 getLastSubstrSplittedByDot() 这两个函数的访问权限设置为 protected。这样做的目的是,可以直接在单元测试中通过对象来调用两个函数进行测试。
- 给 generateRandomAlphameric() 和 getLastSubstrSplittedByDot() 两个函数添加 Google Guava 的 annotation @VisibleForTesting。这个 annotation 没有任何实际的作用,只起到标识的作用,告诉其他人说,这两个函数本该是 private 访问权限的,之所以提升访问权限到 protected,只是为了测试,只能用于单元测试中。
- 打印日志的 Logger 对象被定义为 static final 的,并且在类内部创建,但对于 Logger 对象来说,我们只往里写入数据,并不读取数据,不参与业务逻辑的执行,不会影响代码逻辑的正确性,所以,我们没有必要 mock Logger 对象。
- 除此之外,一些只是为了存储数据的值对象,比如 String、Map、UseVo,我们也没必要通过依赖注入的方式来创建,直接在类中通过 new 创建就可以了。
第三轮重构:编写完善的单元测试
- 针对同一份 generate() 函数的代码实现,我们可以有 3 种不同的功能定义,对应 3 种不同的单元测试:
- 如果我们把 generate() 函数的功能定义为:“生成一个随机唯一 ID”,那我们只要测试多次调用 generate() 函数生成的 ID 是否唯一即可。
- 如果我们把 generate() 函数的功能定义为:“生成一个只包含数字、大小写字母和中划线的唯一 ID”,那我们不仅要测试 ID 的唯一性,还要测试生成的 ID 是否只包含数字、大小写字母和中划线。
- 如果我们把 generate() 函数的功能定义为:“生成唯一 ID,格式为:{主机名 substr}-{时间戳}-{8 位随机数}。在主机名获取失败时,返回:null-{时间戳}-{8 位随机数}”,那我们不仅要测试 ID 的唯一性,还要测试生成的 ID 是否完全符合格式要求。
- 再来看下 getLastfieldOfHostName() 函数:
- 这个函数不容易测试,因为它调用了一个静态函数(InetAddress.getLocalHost().getHostName();),并且这个静态函数依赖运行环境。但是,这个函数的实现非常简单,肉眼基本上可以排除明显的 bug,所以我们可以不为其编写单元测试代码。毕竟,我们写单元测试的目的是为了减少代码 bug,而不是为了写单元测试而写单元测试。
- 如果你真的想要对它进行测试,我们也是有办法的:
- 一种办法是使用更加高级的测试框架。比如 PowerMock,它可以 mock 静态函数。
- 另一种方式是将获取本机名的逻辑再封装为一个新的函数。不过会造成代码过度零碎,也会稍微影响到代码的可读性,这个需要你自己去权衡利弊来做选择。
第四轮重构:添加注释
- 前面我们提到的对于 generate() 函数的 3 种功能定义,就无法用命名来体现,需要补充到注释里面。
- 主要就是写清楚:做什么、为什么、怎么做、怎么用,对一些边界条件、特殊情况进行说明,以及对函数输入、输出、异常进行说明。
本文是王争《设计模式之美》的学习笔记,关注于代码重构和测试。首先,通过提高代码可读性,将ID生成器进行重命名并抽象出接口。然后,改善代码可测试性,将静态函数改为实例方法,优化依赖。接着,编写单元测试以确保不同功能定义下的ID生成正确。最后,完善注释以明确函数目的和使用方式。
:手把手带你将ID生成器代码从“能用”重构为“好用”&spm=1001.2101.3001.5002&articleId=108364550&d=1&t=3&u=35b7384e94de4b948940f1beb6e4c9ec)
2136

被折叠的 条评论
为什么被折叠?



