王争《设计模式之美》学习笔记
ID 生成器需求背景介绍
- “ID”中文翻译为“标识(Identifier)”。
- ID 常用来表示一些业务信息的唯一标识,比如订单的单号或者数据库中的唯一主键,比如地址表中的 ID 字段(实际上是没有业务含义的,对用户来说是透明的,不需要关注)。
- 文中举例,ID可用于log_id或者微服务调用链追踪,就是用唯一id来追踪用户一次完成访问轨迹。
一份“能用”的代码实现
- 文中实现了一个 IdGenerator 类。
- 类中方法 generate() 实现一个ID的生成,包括三部分:
- 本机名的最后一个字段。
- 当前时间戳,精确到毫秒。
- 8 位的随机字符串,包含大小写字母和数字。
- 此方法生成的ID有非常低的重复概率,但如果用于日志追踪足够了。
如何发现代码质量问题?
审视代码的几方面
- 目录设置是否合理、模块划分是否清晰、代码结构是否满足“高内聚、松耦合”?
- 是否遵循经典的设计原则和设计思想(SOLID、DRY、KISS、YAGNI、LOD 等)?
- 设计模式是否应用得当?是否有过度设计?
- 代码是否容易扩展?如果要添加新功能,是否容易实现?
- 代码是否可以复用?是否可以复用已有的项目代码或类库?是否有重复造轮子?
- 代码是否容易测试?单元测试是否全面覆盖了各种正常和异常的情况?
- 代码是否易读?是否符合编码规范(比如命名和注释是否恰当、代码放个是否一致等)?
关注代码实现是否满足业务本身特有的功能和非功能需求
- 代码是否实现了预期的业务需求?
- 逻辑是否正确?是否处理了各种异常情况?
- 日志打印是否得当?是否方便 debug 排查问题?
- 接口是否易用?是否支持幂等、事务等?
- 代码是否存在并发问题?是否线程安全?
- 性能是否有优化空间,比如,SQL、算法是否可以优化?
- 是否有安全漏洞?比如输入输出校验是否全面?
参照以上分析示例代码
审视代码的几方面
- IdGenerator 的代码比较简单,只有一个类,所以,不涉及目录设置、模块划分、代码结构问题,也不违反基本的 SOLID、DRY、KISS、YAGNI、LOD 等设计原则。
- 它没有应用设计模式,所以也不存在不合理使用和过度设计的问题。
- IdGenerator 设计成了实现类而非接口,调用者直接依赖实现而非接口,违反基于接口而非实现编程的设计思想。
- 将 IdGenerator 设计成实现类,而不定义接口,问题也不大。如果哪天 ID 生成算法改变了,我们只需要直接修改实现类的代码就可以。
- 如果项目中需要同时存在两种 ID 生成算法,我们就需要将 IdGenerator 定义为接口,并且为不同的生成算法定义不同的实现类。
- 把 IdGenerator 的 generate() 函数定义为静态函数,会影响使用该函数的代码的可测试性。
- generate() 函数的代码实现依赖运行环境(本机名)、时间函数、随机函数,所以 generate() 函数本身的可测试性也不好,需要做比较大的重构。
- 没有编写单元测试代码,我们需要在重构时对其进行补充。
- 虽然 IdGenerator 只包含一个函数,并且代码行数也不多,但代码的可读性并不好。特别是随机字符串生成的那部分代码:
- 一方面,代码完全没有注释,生成算法比较难读懂。
- 另一方面,代码里有很多魔法数,严重影响代码的可读性。
关注代码实现是否满足业务本身特有的功能和非功能需求
- 获取 hostName 这部分代码逻辑貌似有点问题,并未处理“hostName 为空”的情况。
- 尽管代码中针对获取不到本机名的情况做了异常处理,但是小王对异常的处理是在 IdGenerator 内部将其吐掉,然后打印一条报警日志,并没有继续往上抛出。(此处遗留思考点,后面章节讲解。)
- 日志打印得当,日志描述能够准确反应问题,方便 debug,并且没有过多的冗余日志。
- IdGenerator 只暴露一个 generate() 接口供使用者使用,接口的定义简单明了,不存在不易用问题。
- generate() 函数代码中没有涉及共享变量,所以代码线程安全,多线程环境下调用 generate() 函数不存在并发问题。
- 性能方面,ID 的生成不依赖外部存储,在内存中生成,并且日志的打印频率也不会很高,所以在性能方面足以应对目前的营用场景。
- 每次生成 ID 都需要获取本机名,获取主机名会比较耗时,所以,这部分可以考虑优化一下。
- randomAscii 的范围是 0~122,但可用数字仅包含三段子区间(09,az,A~Z),极端情况下会随机生成很多三段区间之外的无效数字,需要循环很多次才能生成随机字符串的生成算法也可以优化一下。