【Java设计模式 规范与重构】 五 重构实战:基于ID生成器case(上)

简介: 【Java设计模式 规范与重构】 五 重构实战:基于ID生成器case(上)

前面学习了这么多重构相关的知识,比如:持续重构、单元测试、代码的可测试性、解耦、编码规范。都是在理论层面,而重构本身是一个实战的事儿,所以如何将这些理论运用到实践中至关重要,否则只能是纸上谈兵。今天用一个案例case来串联之前学习的重构,来做到融汇贯通。

CASE背景

后端开发中,为了方便在请求出错时排查问题,我们在编写代码的时候会在关键路径上打印日志。某个请求出错之后,希望能搜索出这个请求对应的所有日志,以此来查找问题的原因。而实际情况是,在日志文件中,不同请求的日志会交织在一起。如果没有东西来标识哪些日志属于同一个请求,就无法关联同一个请求的所有日志。每次打印日志的时候可以从请求上下文中取出请求 ID,跟日志一块输出。这样,同一个请求的所有日志都包含同样的请求 ID 信息,就可以通过请求 ID 来搜索同一个请求的所有日志了。ID生成器的简单设计的代码如下:

package com.example.jackson.service;
import com.sun.org.slf4j.internal.Logger;
import com.sun.org.slf4j.internal.LoggerFactory;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Random;
/**
 1. @author tianmaolin004
 2. @date 2022/9/24
 */
public class IdGenerator {
    private static final Logger logger = LoggerFactory.getLogger(IdGenerator.class);
    public static String generate() {
        String id = "";
        try {
            String hostName = InetAddress.getLocalHost().getHostName();
            String[] tokens = hostName.split("\\.");
            if (tokens.length > 0) {
                hostName = tokens[tokens.length - 1];
            }
            char[] randomChars = new char[8];
            int count = 0;
            Random random = new Random();
            while (count < 8) {
                int randomAscii = random.nextInt(122);
                if (randomAscii >= 48 && randomAscii <= 57) {
                    randomChars[count] = (char)('0' + (randomAscii - 48));
                    count++;
                } else if (randomAscii >= 65 && randomAscii <= 90) {
                    randomChars[count] = (char)('A' + (randomAscii - 65));
                    count++;
                } else if (randomAscii >= 97 && randomAscii <= 122) {
                    randomChars[count] = (char)('a' + (randomAscii - 97));
                    count++;
                }
            }
            id = String.format("%s-%d-%s", hostName,
                    System.currentTimeMillis(), new String(randomChars));
        } catch (UnknownHostException e) {
            logger.warn("Failed to get the host name.", e);
        }
        return id;
    }
}

整个 ID 由三部分组成。第一部分是本机名的最后一个字段。第二部分是当前时间戳,精确到毫秒。第三部分是 8 位的随机字符串,包含大小写字母和数字,打印测试如下:

代码诊断

代码问题的排查分为常规检查和业务检查

常规检查CheckList

从通用的标准,也就是本专栏追求的最高目标就是:看这段代码是否可读、可扩展、可维护、灵活、简洁、可复用、可测试,具体的判断标准就是:

  1. 目录设置是否合理、模块划分是否清晰、代码结构是否合理整体结构考虑OK么
  2. 是否遵循经典的设计原则(SOLID、DRY、KISS、YAGNI、LOD)、设计思想(封装、继承、抽象、多态、控制反转、高内聚-松耦合、基于接口而非实现编程,多用组合少用继承)设计模式是否应用得当,是否有过度设计有明显违反理论的地方么
  3. 代码是否容易扩展,如果要添加新功能,是否容易实现好改么,好加功能么
  4. 代码是否可以复用,是否可以复用已有的项目代码或类库?是否有重复造轮子(违反DRY)能抽出来么,通用么
  5. 代码是否容易测试,单元测试是否全面覆盖了各种正常和异常的情况好测试么,mock难度高么
  6. 代码是否易读,是否符合编码规范(比如命名和注释是否恰当、代码风格是否一致等)命名注释ok么,代码风格和编码技巧ok么

第1条是从结构划分上考虑,第2条是从理论层面上考虑,后边4条是从编码感觉上考虑。说到设计原则,这里顺便再回顾下之前学习的设计原则

设计原则 应用范围 看待视角 解决什么问题
SRP单一职责原则 模块、类(接口)、方法 实体自身视角 提高代码的内聚性,可复用性、可读性、可维护性
OCP开闭原则 模块、类(接口)、方法 实体自身视角 提高代码的可扩展
LSP里氏替换原则 类(接口) 父子间关系视角 指导子类设计、继承设计
ISP接口隔离原则 接口 实体间关系视角 降低调用者或使用者依赖,提升接口的可复用性、可读性、可维护性
DIP依赖反转原则 模块 框架设计视角 指导框架设计
KISS保持简单原则 方法 全局视角 提高代码的简洁性、可读性、可维护性
YAGNI勿过度设计原则 类(接口)、方法 全局视角 不过度设计只预留扩展防止降低代码的可读性和可维护性
DRY勿重复编码原则 类(接口)、方法 全局视角 去除重复代码 防止降低代码的可读性和可维护性
LOD迪米特法则 类(接口) 实体间关系视角 防止类间过度依赖,降低代码的耦合性,提高代码的可读性和可维护性

参照常规检查对ID生成器进行检查

  1. IdGenerator 的代码比较简单,只有一个类,所以,不涉及目录设置、模块划分、代码结构问题
  2. IdGenerator 功能单一,不违反基本的 SOLID、DRY、KISS、YAGNI、LOD 等设计原则。它没有应用设计模式,所以也不存在不合理使用和过度设计的问题。不过IdGenerator 设计成了实现类而非接口,调用者直接依赖实现而非接口,违反基于接口而非实现编程的设计思想
  3. IdGenerator功能单一,没有复杂分支,也暂时不需要扩展
  4. IdGenerator生成业务要求格式的唯一ID,所以没有重复造轮子
  5. IdGenerator 的 generate 函数定义为静态函数,会影响使用该函数的代码的可测试性。同时,generate 函数的代码实现依赖运行环境(本机名)、时间函数、随机函数,所以 generate函数本身的可测试性也不好,需要做比较大的重构。代码可测试性不高,同时也没有单元测试
  6. 虽然 IdGenerator 只包含一个函数,并且代码行数也不多,但代码的可读性并不好。特别是随机字符串生成的那部分代码,一方面,代码完全没有注释,生成算法比较难读懂,另一方面,代码里有很多魔法数,严重影响代码的可读性。代码可读性差,不规范

以上为通用checklist检查出来的问题

业务检查CheckList

以上是一些通用的关注点,可以作为常规检查项,套用在任何代码的重构上。除此之外,我们还要关注代码实现是否满足业务本身特有的功能和非功能需求

  1. 代码是否实现了预期的业务需求,功能满足么
  2. 逻辑是否正确,是否处理了各种异常情况,异常情况都hold住么,异常抛出方式合理么
  3. 日志打印是否得当,是否方便 debug 排查问题,好查问题么
  4. 接口是否易用,是否支持幂等、事务等,简明稳定么,可以重复调用么
  5. 代码是否存在并发问题,是否线程安全,高并发下扛的住么
  6. 性能是否有优化空间,比如,SQL、算法是否可以优化,性能还能更好么
  7. 是否有安全漏洞,比如输入输出校验是否全面?质量高么,容易出错么

有了以上的checklist就可以来校验代码到底行不行了。

  1. 虽然代码生成的 ID 并非绝对的唯一,但是对于追踪打印日志来说,是可以接受小概率 ID 冲突的,满足预期的业务需求
  2. 获取 hostName 这部分代码逻辑有点问题,并未处理hostName为空的情况,同时尽管代码中针对获取不到本机名的情况做了异常处理,但是对异常的处理是在 IdGenerator 内部将其吐掉,然后打印一条报警日志,并没有继续往上抛出,没有处理所有异常情况,异常抛出的方式也不合理
  3. 日志打印得当,日志描述能够准确反应问题,方便 debug,并且没有过多的冗余日志
  4. IdGenerator 只暴露一个 generate() 接口供使用者使用,接口的定义简单明了,不存在不易用问题
  5. generate函数代码中没有涉及共享变量,所以代码线程安全,多线程环境下调用 generate函数不存在并发问题
  6. ID 的生成不依赖外部存储,在内存中生成,并且日志的打印频率也不会很高,所以性能没有大问题,有些小问题
  • 每次生成 ID 都需要获取本机名,获取主机名会比较耗时,这部分可以考虑优化一下。
  • randomAscii 的范围是 0~122,但可用数字仅包含三段子区间(0~9,a~z,A~Z),极端情况下会随机生成很多三段区间之外的无效数字,需要循环很多次才能生成随机字符串,所以随机字符串的生成算法也可以优化一下
  1. 没有输入参数,没有大的安全漏洞。

经过常规和业务两轮代码诊断后,可以着手重构处理这些问题

代码重构

问题分析出来后,开始基于重构的手段进行重构。重构要循序渐进、小步快跑,每次改动一点点,改好之后,再进行下一轮的优化,保证每次对代码的改动不会过大,能在很短的时间内完成。

  • 第一轮重构:提高代码的可读性:扩展接口,命名优化,代码优化
  • 第二轮重构:提高代码的可测试性:干掉静态方法,剥离依赖,减少方法未决行为
  • 第三轮重构:编写完善的单元测试
  • 第四轮重构:提高代码的可读性:重构拆分方法后添加注释
  • 第五轮重构:正确处理异常和抛出异常

接下来按照重构计划有序执行!

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

首先,要解决最明显、最急需改进的代码可读性问题。具体有下面几点:

  1. 对 IdGenerator 类重命名,并且抽象出对应的接口。IdGenerator为整体功能接口,LogTraceIdGenerator继承IdGenerator,从功能的角度上进行细分,然后实现类从实现方式的角度实现接口 LogTraceIdGenerator,命名为 RandomIdGenerator即可,实现方式还可以拓展,例如:SequenceIdGenerator
  2. hostName 变量不应该被重复使用,尤其当这两次使用时的含义还不同的时候;
  3. 将获取 hostName 的代码抽离出来,定义为 getLastfieldOfHostName 函数;把代码分割成更小的单元块
  4. 删除代码中的魔法数,比如,57、90、97、122;使用解释型变量
  5. 将随机数生成的代码抽离出来,定义为 generateRandomAlphameric函数;把代码分割成更小的单元块
  6. generate() 函数中的三个 if 逻辑重复且实现过于复杂,要对其进行简化;使用解释型变量

这部分对应于上篇规范相关Blog: 【Java设计模式 规范与重构】 四 小型重构的手段:规范的二十条军规,整体代码重构如下:

package com.example.jackson.service;
/**
 * @author tianmaolin004
 * @date 2022/9/24
 */
public interface IdGenerator {
    String generate();
}
package com.example.jackson.service;
/**
 * @author tianmaolin004
 * @date 2022/9/24
 */
public interface LogTraceIdGenerator extends IdGenerator{
}
package com.example.jackson.service;
import com.sun.org.slf4j.internal.Logger;
import com.sun.org.slf4j.internal.LoggerFactory;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Random;
/**
 * @author tianmaolin004
 * @date 2022/9/24
 */
public class RandomIdGenerator implements LogTraceIdGenerator {
    private static final Logger logger = LoggerFactory.getLogger(RandomIdGenerator.class);
    @Override
    public String generate() {
        String substrOfHostName = getLastFieldOfHostName();
        long currentTimeMillis = System.currentTimeMillis();
        String randomString = generateRandomAlphameric(8);
        return String.format("%s-%d-%s", substrOfHostName, currentTimeMillis, randomString);
    }
    private String getLastFieldOfHostName() {
        try {
            String hostName = InetAddress.getLocalHost().getHostName();
            String[] tokens = hostName.split("\\.");
            return tokens[tokens.length - 1];
        } catch (UnknownHostException e) {
            logger.warn("Failed to get the host name.", e);
        }
        return null;
    }
    private String generateRandomAlphameric(int length) {
        char[] randomChars = new char[length];
        int count = 0;
        Random random = new Random();
        while (count < length) {
            int randomAscii = random.nextInt('z');
            boolean isDigit = randomAscii >= '0' && randomAscii <= '9';
            boolean isUppercase = randomAscii >= 'A' && randomAscii <= 'Z';
            boolean isLowercase = randomAscii >= 'a';
            if (isDigit || isUppercase || isLowercase) {
                randomChars[count] = (char) (randomAscii);
                ++count;
            }
        }
        return new String(randomChars);
    }
}
public static void main(String[] args) {
        IdGenerator idGenerator = new RandomIdGenerator();
        System.out.println(idGenerator.generate());
    }

打印结果为:

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
18小时前
|
Java
Java 编程风格与规范:提升代码质量与可维护性
【4月更文挑战第27天】在软件开发过程中,良好的代码规范和一致的编程风格对于团队协作、代码可读性和可维护性至关重要。Java 作为一种流行的编程语言,拥有丰富的编码规范和最佳实践。
8 2
|
18小时前
|
设计模式 消息中间件 Java
Java 设计模式:探索发布-订阅模式的原理与应用
【4月更文挑战第27天】发布-订阅模式是一种消息传递范式,被广泛用于构建松散耦合的系统。在 Java 中,这种模式允许多个对象监听和响应感兴趣的事件。
8 2
|
19小时前
|
设计模式 算法 Java
Java 设计模式:探索策略模式的概念和实战应用
【4月更文挑战第27天】策略模式是一种行为设计模式,它允许在运行时选择算法的行为。在 Java 中,策略模式通过定义一系列的算法,并将每一个算法封装起来,并使它们可以互换,这样算法的变化不会影响到使用算法的客户。
6 1
|
1天前
|
设计模式 安全 Java
【JAVA】Java 中什么叫单例设计模式?请用 Java 写出线程安全的单例模式
【JAVA】Java 中什么叫单例设计模式?请用 Java 写出线程安全的单例模式
|
3天前
|
设计模式 算法 Java
[设计模式Java实现附plantuml源码~行为型]定义算法的框架——模板方法模式
[设计模式Java实现附plantuml源码~行为型]定义算法的框架——模板方法模式
|
3天前
|
设计模式 JavaScript Java
[设计模式Java实现附plantuml源码~行为型] 对象状态及其转换——状态模式
[设计模式Java实现附plantuml源码~行为型] 对象状态及其转换——状态模式
|
2天前
|
数据采集 存储 Java
高德地图爬虫实践:Java多线程并发处理策略
高德地图爬虫实践:Java多线程并发处理策略
|
5天前
|
安全 Java 调度
Java线程:深入理解与实战应用
Java线程:深入理解与实战应用
24 0
|
1天前
|
安全 Java
【JAVA】线程的run()和start()有什么区别?
【JAVA】线程的run()和start()有什么区别?
|
2天前
|
缓存 Java
Java并发编程:深入理解线程池
【4月更文挑战第26天】在Java中,线程池是一种重要的并发工具,它可以有效地管理和控制线程的执行。本文将深入探讨线程池的工作原理,以及如何使用Java的Executor框架来创建和管理线程池。我们将看到线程池如何提高性能,减少资源消耗,并提供更好的线程管理。