【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());
    }

打印结果为:

相关实践学习
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
相关文章
|
2月前
|
设计模式 算法 搜索推荐
Java 设计模式之策略模式:灵活切换算法的艺术
策略模式通过封装不同算法并实现灵活切换,将算法与使用解耦。以支付为例,微信、支付宝等支付方式作为独立策略,购物车根据选择调用对应支付逻辑,提升代码可维护性与扩展性,避免冗长条件判断,符合开闭原则。
373 35
|
2月前
|
设计模式 消息中间件 传感器
Java 设计模式之观察者模式:构建松耦合的事件响应系统
观察者模式是Java中常用的行为型设计模式,用于构建松耦合的事件响应系统。当一个对象状态改变时,所有依赖它的观察者将自动收到通知并更新。该模式通过抽象耦合实现发布-订阅机制,广泛应用于GUI事件处理、消息通知、数据监控等场景,具有良好的可扩展性和维护性。
286 8
|
2月前
|
设计模式 网络协议 数据可视化
Java 设计模式之状态模式:让对象的行为随状态优雅变化
状态模式通过封装对象的状态,使行为随状态变化而改变。以订单为例,将待支付、已支付等状态独立成类,消除冗长条件判断,提升代码可维护性与扩展性,适用于状态多、转换复杂的场景。
331 0
|
2月前
|
设计模式 Java Spring
Java 设计模式之责任链模式:优雅处理请求的艺术
责任链模式通过构建处理者链,使请求沿链传递直至被处理,实现发送者与接收者的解耦。适用于审批流程、日志处理等多级处理场景,提升系统灵活性与可扩展性。
294 2
|
2月前
|
JSON 网络协议 安全
【Java】(10)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
189 1
|
2月前
|
JSON 网络协议 安全
【Java基础】(1)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
213 1
|
3月前
|
数据采集 存储 弹性计算
高并发Java爬虫的瓶颈分析与动态线程优化方案
高并发Java爬虫的瓶颈分析与动态线程优化方案
Java 数据库 Spring
164 0
|
3月前
|
算法 Java
Java多线程编程:实现线程间数据共享机制
以上就是Java中几种主要处理多线程序列化资源以及协调各自独立运行但需相互配合以完成任务threads 的技术手段与策略。正确应用上述技术将大大增强你程序稳定性与效率同时也降低bug出现率因此深刻理解每项技术背后理论至关重要.
266 16
|
4月前
|
缓存 并行计算 安全
关于Java多线程详解
本文深入讲解Java多线程编程,涵盖基础概念、线程创建与管理、同步机制、并发工具类、线程池、线程安全集合、实战案例及常见问题解决方案,助你掌握高性能并发编程技巧,应对多线程开发中的挑战。