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 值判断。
对于函数抛出的异常,我们有三种处理方法:直接吞掉、直接往上抛出、包裹成新的异常抛出
「真诚赞赏,手留余香」
真诚赞赏,手留余香
使用微信扫描二维码完成支付
