【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日志并进行多维度分析。
相关文章
|
3天前
|
设计模式 Java 关系型数据库
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析
本文是“Java学习路线”专栏的导航文章,目标是为Java初学者和初中高级工程师提供一套完整的Java学习路线。
|
3天前
|
设计模式 安全 算法
【Java面试题汇总】设计模式篇(2023版)
谈谈你对设计模式的理解、七大原则、单例模式、工厂模式、代理模式、模板模式、观察者模式、JDK中用到的设计模式、Spring中用到的设计模式
【Java面试题汇总】设计模式篇(2023版)
|
3天前
|
设计模式 Java 关系型数据库
【Java笔记+踩坑】设计模式——原型模式
对比原型模式和传统方式的实现思路、代码方案、优缺点,阐述原型模式的使用场景,以及深拷贝、浅拷贝等相关概念,并扩展原型模式在Spring源码中的应用。
【Java笔记+踩坑】设计模式——原型模式
|
10天前
|
设计模式 C# 开发者
C#设计模式入门实战教程
C#设计模式入门实战教程
|
19天前
|
设计模式 缓存 算法
揭秘策略模式:如何用Java设计模式轻松切换算法?
【8月更文挑战第30天】设计模式是解决软件开发中特定问题的可重用方案。其中,策略模式是一种常用的行为型模式,允许在运行时选择算法行为。它通过定义一系列可互换的算法来封装具体的实现,使算法的变化与客户端分离。例如,在电商系统中,可以通过定义 `DiscountStrategy` 接口和多种折扣策略类(如 `FidelityDiscount`、`BulkDiscount` 和 `NoDiscount`),在运行时动态切换不同的折扣逻辑。这样,`ShoppingCart` 类无需关心具体折扣计算细节,只需设置不同的策略即可实现灵活的价格计算,符合开闭原则并提高代码的可维护性和扩展性。
36 2
|
19天前
|
设计模式 Java
Java 设计模式之谜:工厂模式与抽象工厂模式究竟隐藏着怎样的神奇力量?
【8月更文挑战第30天】在Java编程中,设计模式为常见问题提供了高效解决方案。工厂模式与抽象工厂模式是常用的对象创建型设计模式,能显著提升代码的灵活性、可维护性和可扩展性。工厂模式通过定义创建对象的接口让子类决定实例化哪个类;而抽象工厂模式则进一步提供了一个创建一系列相关或相互依赖对象的接口,无需指定具体类。这种方式使得系统更易于扩展和维护。
29 1
|
17天前
|
前端开发 C# 设计模式
“深度剖析WPF开发中的设计模式应用:以MVVM为核心,手把手教你重构代码结构,实现软件工程的最佳实践与高效协作”
【8月更文挑战第31天】设计模式是在软件工程中解决常见问题的成熟方案。在WPF开发中,合理应用如MVC、MVVM及工厂模式等能显著提升代码质量和可维护性。本文通过具体案例,详细解析了这些模式的实际应用,特别是MVVM模式如何通过分离UI逻辑与业务逻辑,实现视图与模型的松耦合,从而优化代码结构并提高开发效率。通过示例代码展示了从模型定义、视图模型管理到视图展示的全过程,帮助读者更好地理解并应用这些模式。
30 0
|
17天前
|
前端开发 开发者 开发框架
JSF与Bootstrap,打造梦幻响应式网页!让你的应用跨设备,让用户爱不释手!
【8月更文挑战第31天】在现代Web应用开发中,响应式设计至关重要,以确保不同设备上的良好用户体验。本文探讨了JSF(JavaServer Faces)与Bootstrap框架的结合使用,展示了如何构建响应式网页。JSF是一个基于Java的Web应用框架,提供丰富的UI组件和表单处理功能;而Bootstrap则是一个基于HTML、CSS和JavaScript的前端框架,专注于实现响应式设计。通过结合两者的优势,开发者能够更便捷地创建自适应布局,提升Web应用体验。然而,这种组合也有其局限性,如JSF组件库较小和较高的学习成本等,因此在选择开发框架时需综合考虑具体需求和应用场景。
29 0
|
17天前
|
设计模式 前端开发 开发者
Angular携手Material Design:探索设计模式下的UI组件开发之道——从按钮到对话框的全面实战演示
【8月更文挑战第31天】在现代Web应用开发中,Angular框架结合Material Design设计原则与组件库,显著提升了用户界面的质量与开发效率。本文通过具体代码示例,详细介绍如何在Angular项目中引入并使用Material Design的UI组件,包括按钮、表单和对话框等,帮助开发者快速构建美观且功能强大的应用。通过这种方式,不仅能提高开发效率,还能确保界面设计的一致性和高质量,为用户提供卓越的体验。
19 0
|
3天前
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
【Java面试题汇总】多线程、JUC、锁篇(2023版)