【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日志并进行多维度分析。
相关文章
|
25天前
|
设计模式 消息中间件 搜索推荐
Java 设计模式——观察者模式:从优衣库不使用新疆棉事件看系统的动态响应
【11月更文挑战第17天】观察者模式是一种行为设计模式,定义了一对多的依赖关系,使多个观察者对象能直接监听并响应某一主题对象的状态变化。本文介绍了观察者模式的基本概念、商业系统中的应用实例,如优衣库事件中各相关方的动态响应,以及模式的优势和实际系统设计中的应用建议,包括事件驱动架构和消息队列的使用。
|
1月前
|
设计模式 前端开发 JavaScript
JavaScript设计模式及其在实战中的应用,涵盖单例、工厂、观察者、装饰器和策略模式
本文深入探讨了JavaScript设计模式及其在实战中的应用,涵盖单例、工厂、观察者、装饰器和策略模式,结合电商网站案例,展示了设计模式如何提升代码的可维护性、扩展性和可读性,强调了其在前端开发中的重要性。
29 2
|
1月前
|
设计模式 Java 数据库连接
Java编程中的设计模式:单例模式的深度剖析
【10月更文挑战第41天】本文深入探讨了Java中广泛使用的单例设计模式,旨在通过简明扼要的语言和实际示例,帮助读者理解其核心原理和应用。文章将介绍单例模式的重要性、实现方式以及在实际应用中如何优雅地处理多线程问题。
38 4
|
2月前
|
设计模式 Java 程序员
[Java]23种设计模式
本文介绍了设计模式的概念及其七大原则,强调了设计模式在提高代码重用性、可读性、可扩展性和可靠性方面的作用。文章还简要概述了23种设计模式,并提供了进一步学习的资源链接。
54 0
[Java]23种设计模式
|
1月前
|
设计模式 JavaScript Java
Java设计模式:建造者模式详解
建造者模式是一种创建型设计模式,通过将复杂对象的构建过程与表示分离,使得相同的构建过程可以创建不同的表示。本文详细介绍了建造者模式的原理、背景、应用场景及实际Demo,帮助读者更好地理解和应用这一模式。
|
2天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
2天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
3天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
14 3
|
3天前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
30 2
|
11天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
42 6