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

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 【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日志并进行多维度分析。
相关文章
|
29天前
|
存储 Java 开发者
Java Map实战:用HashMap和TreeMap轻松解决复杂数据结构问题!
【10月更文挑战第17天】本文深入探讨了Java中HashMap和TreeMap两种Map类型的特性和应用场景。HashMap基于哈希表实现,支持高效的数据操作且允许键值为null;TreeMap基于红黑树实现,支持自然排序或自定义排序,确保元素有序。文章通过具体示例展示了两者的实战应用,帮助开发者根据实际需求选择合适的数据结构,提高开发效率。
60 2
|
7天前
|
Java
java switch case多个条件
通过本文的介绍,我们详细探讨了Java中 `switch case`语句的多种用法和优化方法。从基本语法到合并多个条件,再到使用枚举、常量和函数优化,`switch case`语句在Java编程中提供了一种灵活且高效的控制流方式。掌握这些技巧,能够编写出更加简洁、可读性强的代码,提高开发效率和代码质量。希望本文能为您在实际开发中提供有价值的参考和指导。
20 2
|
1月前
|
存储 消息中间件 安全
JUC组件实战:实现RRPC(Java与硬件通过MQTT的同步通信)
【10月更文挑战第9天】本文介绍了如何利用JUC组件实现Java服务与硬件通过MQTT的同步通信(RRPC)。通过模拟MQTT通信流程,使用`LinkedBlockingQueue`作为消息队列,详细讲解了消息发送、接收及响应的同步处理机制,包括任务超时处理和内存泄漏的预防措施。文中还提供了具体的类设计和方法实现,帮助理解同步通信的内部工作原理。
JUC组件实战:实现RRPC(Java与硬件通过MQTT的同步通信)
|
1月前
|
开发框架 Java 程序员
揭开Java反射的神秘面纱:从原理到实战应用!
本文介绍了Java反射的基本概念、原理及应用场景。反射允许程序在运行时动态获取类的信息并操作其属性和方法,广泛应用于开发框架、动态代理和自定义注解等领域。通过反射,可以实现更灵活的代码设计,但也需注意其性能开销。
47 1
|
2月前
|
缓存 负载均衡 Dubbo
Dubbo技术深度解析及其在Java中的实战应用
Dubbo是一款由阿里巴巴开源的高性能、轻量级的Java分布式服务框架,它致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。
77 6
|
2月前
|
Java
领略Lock接口的风采,通过实战演练,让你迅速掌握这门高深武艺,成为Java多线程领域的武林盟主
领略Lock接口的风采,通过实战演练,让你迅速掌握这门高深武艺,成为Java多线程领域的武林盟主
36 7
|
2月前
|
Java Android开发 C++
🚀Android NDK开发实战!Java与C++混合编程,打造极致性能体验!📊
在Android应用开发中,追求卓越性能是不变的主题。本文介绍如何利用Android NDK(Native Development Kit)结合Java与C++进行混合编程,提升应用性能。从环境搭建到JNI接口设计,再到实战示例,全面展示NDK的优势与应用技巧,助你打造高性能应用。通过具体案例,如计算斐波那契数列,详细讲解Java与C++的协作流程,帮助开发者掌握NDK开发精髓,实现高效计算与硬件交互。
135 1
|
1月前
|
Java 编译器 Android开发
java作业的提交规范与要求
java作业的提交规范与要求
25 0
|
2月前
|
Java 数据中心 微服务
Java高级知识:线程池隔离与信号量隔离的实战应用
在Java并发编程中,线程池隔离与信号量隔离是两种常用的资源隔离技术,它们在提高系统稳定性、防止系统过载方面发挥着重要作用。
45 0
|
2月前
|
设计模式 监控 Java
重构与优化-前言(java)
重构与优化-前言(java)