《设计模式之美》实战一(下):手把手带你将ID生成器代码从“能用”重构为“好用”

本文是王争《设计模式之美》的学习笔记,关注于代码重构和测试。首先,通过提高代码可读性,将ID生成器进行重命名并抽象出接口。然后,改善代码可测试性,将静态函数改为实例方法,优化依赖。接着,编写单元测试以确保不同功能定义下的ID生成正确。最后,完善注释以明确函数目的和使用方式。

王争《设计模式之美》学习笔记

第一轮重构:提高代码的可读性

  • 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 种不同的单元测试:
    1. 如果我们把 generate() 函数的功能定义为:“生成一个随机唯一 ID”,那我们只要测试多次调用 generate() 函数生成的 ID 是否唯一即可。
    2. 如果我们把 generate() 函数的功能定义为:“生成一个只包含数字、大小写字母和中划线的唯一 ID”,那我们不仅要测试 ID 的唯一性,还要测试生成的 ID 是否只包含数字、大小写字母和中划线。
    3. 如果我们把 generate() 函数的功能定义为:“生成唯一 ID,格式为:{主机名 substr}-{时间戳}-{8 位随机数}。在主机名获取失败时,返回:null-{时间戳}-{8 位随机数}”,那我们不仅要测试 ID 的唯一性,还要测试生成的 ID 是否完全符合格式要求。
  • 再来看下 getLastfieldOfHostName() 函数:
    • 这个函数不容易测试,因为它调用了一个静态函数(InetAddress.getLocalHost().getHostName();),并且这个静态函数依赖运行环境。但是,这个函数的实现非常简单,肉眼基本上可以排除明显的 bug,所以我们可以不为其编写单元测试代码。毕竟,我们写单元测试的目的是为了减少代码 bug,而不是为了写单元测试而写单元测试。
    • 如果你真的想要对它进行测试,我们也是有办法的:
      • 一种办法是使用更加高级的测试框架。比如 PowerMock,它可以 mock 静态函数。
      • 另一种方式是将获取本机名的逻辑再封装为一个新的函数。不过会造成代码过度零碎,也会稍微影响到代码的可读性,这个需要你自己去权衡利弊来做选择。

第四轮重构:添加注释

  • 前面我们提到的对于 generate() 函数的 3 种功能定义,就无法用命名来体现,需要补充到注释里面。
  • 主要就是写清楚:做什么、为什么、怎么做、怎么用,对一些边界条件、特殊情况进行说明,以及对函数输入、输出、异常进行说明。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值