ID生成器代码重构

Posted by WZhong on Saturday, January 25, 2020

TOC

重构就是发现代码质量问题,并且对其进行优化的过程。

如何发现代码质量问题

常规checklist:可读、可扩展、可维护、灵活、简洁、可复用、可测试等

  • 目录设置是否合理、模块划分是否清晰、代码结构是否满足“高内聚、低耦合”?

  • 是否遵循经典的设计原则和设计思想(SOLID、DRY、KISS、YAGNI、LOD等)?

  • 设计模式是否应用得当,是否有过度设计?

  • 代码是否易扩展?如果要添加新功能,是否容易实现?

  • 代码是否可复用?是否可以复用已有项目代码或类库?是否有重复造轮子?

  • 代码是否容易测试?单元测试是否全面覆盖了各种正常和异常情况?

  • 代码是否易读?是否符合编码规范(如命名和注释是否恰当、代码风格是否一致)?

业务需求checklist:业务本身特有的功能和非功能需求

  • 代码是否实现了预期的业务需求?

  • 逻辑是否正确?是否处理了各种异常情况?

  • 日志打印是否得当?是否方便debug排查问题?

  • 接口是否易用?是否支持幂等、事务等?

  • 代码是否存在并发问题?是否线程安全?

  • 性能是否有优化空间,如sql、算法是否可以优化?

  • 是否有安全漏洞?如输入输出校验是否全面?

重构过程:循序渐进、小步快跑

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

  • hostName 变量不应该被重复使用,尤其当这两次使用时的含义还不同的时候;
  • 将获取 hostName 的代码抽离出来,定义为 getLastfieldOfHostName() 函数;
  • 删除代码中的魔法数,比如,57、90、97、122;
  • 将随机数生成的代码抽离出来,定义为 generateRandomAlphameric() 函数;
  • generate() 函数中的三个 if 逻辑重复了,且实现过于复杂,我们要对其进行简化;
  • 对 IdGenerator 类重命名,并且抽象出对应的接口。

第二轮重构:提高代码可测试性

  • generate() 函数定义为静态函数,会影响使用该函数的代码的可测试性;
  • generate() 函数的代码实现依赖运行环境(本机名)、时间函数、随机函数,所以 generate() 函数本身的可测试性也不好。
    • 从 getLastfieldOfHostName() 函数中,将逻辑比较复杂的那部分代码剥离出来,定义为 getLastSubstrSplittedByDot() 函数。因为 getLastfieldOfHostName() 函数依赖本地主机名,所以,剥离出主要代码之后这个函数变得非常简单,可以不用测试。我们重点测试 getLastSubstrSplittedByDot() 函数即可。
    • 将 generateRandomAlphameric() 和 getLastSubstrSplittedByDot() 这两个函数的访问权限设置为 protected。这样做的目的是,可以直接在单元测试中通过对象来调用两个函数进行测试。
    • 给 generateRandomAlphameric() 和 getLastSubstrSplittedByDot() 两个函数添加 Google Guava 的 annotation @VisibleForTesting。这个 annotation 没有任何实际的作用,只起到标识的作用,告诉其他人说,这两个函数本该是 private 访问权限的,之所以提升访问权限到 protected,只是为了测试,只能用于单元测试中。

第三轮重构:编写完善的单元测试

  • 为了提高代码的可测试性,已经这两个部分代码跟不可控的组件(本机名、随机函数、时间函数)进行了隔离。所以只需要设计完备的单元测试用例即可

  • 针对 generate() 函数的前两种定义,我们不需要 mock 获取主机名函数、随机函数、时间函数等,但对于第 3 种定义,我们需要 mock 获取主机名函数,让其返回 null,测试代码运行是否符合预期

  • 这个函数不容易测试,因为它调用了一个静态函数(InetAddress.getLocalHost().getHostName();),并且这个静态函数依赖运行环境。但是,这个函数的实现非常简单,肉眼基本上可以排除明显的 bug,所以我们可以不为其编写单元测试代码。毕竟,我们写单元测试的目的是为了减少代码 bug,而不是为了写单元测试而写单元测试

第四轮重构:所有重构完成之后添加注释

  • 主要就是写清楚:做什么、为什么、怎么做、怎么用,对一些边界条件、特殊情况进行说明,以及对函数输入、输出、异常进行说明

错误处理的方式

函数出错应该返回什么

  • 返回错误码

    C 语言没有异常这样的语法机制,返回错误码便是最常用的出错处理方式。而 Java、Python 等比较新的编程语言中,大部分情况下,我们都用异常来处理函数出错的情况,极少会用到错误码

  • 返回NULL值

    在多数编程语言中,我们用 NULL 来表示“不存在”这种语义。对于查找函数来说,数据不存在并非一种异常情况,是一种正常行为,所以返回表示不存在语义的 NULL 值比返回异常更加合理。

  • 返回空对象

    返回 NULL 值有各种弊端,对此有一个比较经典的应对策略,那就是应用空对象设计模式。当函数返回的数据是字符串类型或者集合类型的时候,我们可以用空字符串或空集合替代 NULL 值,来表示不存在的情况。这样,我们在使用函数的时候,就可以不用做 NULL 值判断。

  • 返回异常对象

    返回 NULL 值有各种弊端,对此有一个比较经典的应对策略,那就是应用空对象设计模式。当函数返回的数据是字符串类型或者集合类型的时候,我们可以用空字符串或空集合替代 NULL 值,来表示不存在的情况。这样,我们在使用函数的时候,就可以不用做 NULL 值判断。

    对于函数抛出的异常,我们有三种处理方法:直接吞掉、直接往上抛出、包裹成新的异常抛出

「真诚赞赏,手留余香」

WZhong

真诚赞赏,手留余香

使用微信扫描二维码完成支付