当前位置: 首页 > news >正文

第五章 制作工具优化

第五章 制作工具优化

前言

本笔记主要用途是学习up 主程序员鱼皮的项目:代码生成器时的一些学习心得。
代码地址:https://github.com/Liucc-123/yuzi-generator.git
项目教程:https://www.codefather.cn/course/1790980795074654209
上一章节内容:https://blog.csdn.net/weixin_45284646/article/details/147267667?spm=1001.2014.3001.5501

前情回顾

在上一章节中我们完成了项目的第二阶段 — 开发代码生成器制作工具。

之前我们将本阶段的需求进行了拆解,划分为3 大实现步骤:开发基础的代码生成器制作工具、配置文件增强、工具能力增强。

目前基础的制作工具我们已开发完成,能够根据配置文件自动生成代码生成器项目、自动生成 jar 包和脚本文件。但目前仍存在很多不足,具备很大的优化空间。

本节重点

本章节属于项目的第二阶段 — 优化代码生成器制作工具。

重点内容有:

  1. 可移植性优化
  2. 功能优化
  3. 健壮性优化
  4. 可扩展性优化

一、可移植性优化

什么是可移植性?

目前的制作工具存在一个严重的问题:由于生成代码所以来的原始模板项目并没有打包到代码生成器中,导致我们将代码生成器分享给别人,在另一台电脑上可能是无法使用的。比如代码生成器中的MainGenerator 类的 inputRootPath的路径是这样的,别人的文件系统中可能不存在这样的路径,甚至不是 Windows 操作系统。
在这里插入图片描述

用专业的术语说,也就是说,我们现在的代码生成器不具备可移植性

一般来说,可移植性是指程序可以在不同计算机、操作系统或编程语言环境下均能够正确运行的能力。具备良好可以执行的程序能够在不同环境下正常运行,
而不需要做大量的修改配置工作以适配不同的环境。

对应到代码生成器项目,可以执行实质能够让代码生成器在不同的电脑上直接运行,不需要在人工手动调整模板文件路径。

实现方式

实现思路:
在生成代码生成器的过程中,将源模板项目拷贝到代码生成器的指定目录下,比如.source 。然后 MainGenerator.source 进行读取,这样就避免了硬编码。

实现步骤

在配置文件 meta.json中的 fileConfig 对象新增 sourceRootPath 字段,表示模板文件的路径。inputRootPath 字段的值改为.source相对路径。

同步修改 Meta.java实体类,增加 sourceRootPath 字段:

@NoArgsConstructor
@Data
public static class FileConfig {private String inputRootPath;private String outputRootPath;private String sourceRootPath;private String type;private List<FileInfo> files;...
}

在 MainGenerator 中增加原始模板文件拷贝的逻辑,示例代码如下:

public class MainGenerator {public static void main(String[] args) throws TemplateException, IOException, InterruptedException {...// 输出根路径...// 复制原始文件String sourceRootPath = meta.getFileConfig().getSourceRootPath();String sourceCopyDestPath = outputPath + File.separator + ".source";FileUtil.copy(sourceRootPath, sourceCopyDestPath, false);// 读取 resources 目录...}
}

然后执行测试,成功在生成的项目中复制了原始模板文件:
在这里插入图片描述

可以看到 代码生成器的中的读取模板文件也已调整为相对路径。

尝试执行脚本文件,检查代码生成器是否可以正常工作:
在这里插入图片描述

在这里插入图片描述

二、功能优化

首先优化一下制作工具的基本功能,目标是让我们生成的代码生成器项目更加规范。

1、增加项目介绍文件

标准的开源项目一般都会在根目录下编写一个 README.md项目介绍文件,可以帮助用户快速了解项目的背景、价值、用法等。

因此代码生成器作为一个工具,用法介绍是必不可少的,所以我们让制作工具额外生成一个README.md文件。

实现方式和之前类似,读取元信息使用 FreeMarker 进行动态生成。

预期生成的 README.md文件如下:

在这里插入图片描述

1、打开制作工具项目,在 resources/templates目录下常见 README.md.ftl模板文件,完整代码如下:

# ${name}> ${description}
>
> 作者:${author}
>
> 基于 程序员鱼皮的 [鱼籽代码生成器项目](https://github.com/liyupi/yuzi-generator) 制作,感谢您的使用!可以通过命令行交互式输入的方式动态生成想要的项目代码## 使用说明执行项目根目录下的脚本文件:generator <命令> <选项参数>示例命令:generator generate <#list modelConfig.models as modelInfo>-${modelInfo.abbr} </#list>## 参数说明<#list modelConfig.models as modelInfo>
${modelInfo?index + 1})${modelInfo.fieldName}类型:${modelInfo.type}描述:${modelInfo.description}默认值:${modelInfo.defaultValue?c}缩写: -${modelInfo.abbr}</#list>

2、然后在 MainGenerator 中增加 README.md文件的生成逻辑,追加代码如下:

public class MainGenerator {public static void main(String[] args) throws TemplateException, IOException, InterruptedException {...// README.mdinputFilePath = inputResourcePath + File.separator + "templates/README.md.ftl";outputFilePath = outputPath + File.separator + "README.md";DynamicFileGenerator.doGenerate(inputFilePath , outputFilePath, meta);// 构建 jar 包JarGenerator.doGenerate(outputPath);...}
}

3、测试执行,在项目根目录生成的项目介绍文件:
在这里插入图片描述

2、制作精简版代码生成器

接下来我们对代码生成器的空间占用进行优化:

可以看到代码生成器中不仅包含了实际执行的 jar 包和脚本文件,还包含了生成器源码、target 目录中的编译文件等。

在这里插入图片描述

对于代码生成器的用户来说,可能并不关注这些文件,只要能运行脚本、jar 即可。因此,我们可以生成更为精简的代码生成器,只需要保留 jar 包、脚本文件、原始模板文件。

小技巧:在开发阶段,暂时不需要的代码也记得先保留下来,不要直接删除,万一后期再次需要。

我们不修改原有的代码生成方式,额外增加一套生成精简版代码生成器的逻辑,放到/dist目录下。在 MainGenerator 中新增生成精简版项目的逻辑,示例代码如下:

public class MainGenerator {public static void main(String[] args) throws TemplateException, IOException, InterruptedException {...// 封装脚本String shellOutputFilePath = outputPath + File.separator + "generator";String jarName = String.format("%s-%s-jar-with-dependencies.jar", meta.getName(), meta.getVersion());String jarPath = "target/" + jarName;ScriptGenerator.doGenerate(shellOutputFilePath, jarPath);// 生成精简版的程序(产物包)String distOutputPath = outputPath + "-dist";// - 拷贝 jar 包String targetAbsolutePath = distOutputPath + File.separator + "target";FileUtil.mkdir(targetAbsolutePath);String jarAbsolutePath = outputPath + File.separator + jarPath;FileUtil.copy(jarAbsolutePath, targetAbsolutePath, true);// - 拷贝脚本文件FileUtil.copy(shellOutputFilePath, distOutputPath, true);FileUtil.copy(shellOutputFilePath + ".bat", distOutputPath, true);// - 拷贝源模板文件FileUtil.copy(sourceCopyDestPath, distOutputPath, true);}
}

测试执行,看到生成的项目多了-dist目录,里面仅包含运行代码生成器所必要的文件,更加轻量:

在这里插入图片描述

其他扩展思路

支持git 托管项目

制作工具生成的代码生成器支持 git版本控制工具进行托管,可以根据元信息的配置让开发者选择是否让 git 托管所生成的代码生成器。

实现思路:通过 Process 类执行 git init 命令,并复制Java的.gitignore文件模板文件到代码生成器项目中。

1、元信息 meta.json中添加 git 相关配置

"git": {"enable": false,"gitignore": "/Users/liuchuangchuang/code/yuzi-generator/.gitignore"},

2、Meta 实体类添加相应字段

在这里插入图片描述

3、在制作工具项目的marker/generator新增 GitGenerator 类,使用 Process 类执行 git init 命令,使用 hutool 工具类,拷贝 gitignore 文件到代码生成器中。

package com.liucc.marker.generator;import cn.hutool.core.io.FileUtil;import java.io.File;
import java.io.IOException;/*** git 仓库生成器*/
public class GitGenerator {/*** 使用 git 托管代码生成器* @param projectPath 代码生成器所在文件路径* @param gitignorePath git ignore 文件路径*/public static void doGenerator(String projectPath, String gitignorePath) {try {// 创建一个 ProcessBuilder 实例ProcessBuilder processBuilder = new ProcessBuilder("git", "init");// 设置工作目录processBuilder.directory(new File(projectPath));// 启动进程Process process = processBuilder.start();// 等待进程完成int exitCode = process.waitFor();if (exitCode == 0) {System.out.println("Git repository initialized successfully.");} else {System.err.println("Failed to initialize Git repository. Exit code: " + exitCode);}// 复制.gitignore 文件FileUtil.copy(gitignorePath, projectPath, true);} catch (IOException | InterruptedException e) {e.printStackTrace();}}public static void main(String[] args) {// 示例调用doGenerator("/Users/liuchuangchuang/code/yuzi-generator/yuzi-generator-marker/generated/acm-template-pro-generator","/Users/liuchuangchuang/code/yuzi-generator/.gitignore");}
}

4、在 MainGenerator 类中新增逻辑,调用 GitGenerator 托管代码生成器。示例代码如下:

public class MainGenerator {public static void main(String[] args) throws TemplateException, IOException, InterruptedException {// ...// 使用 git 托管项目if (meta.getGit().getEnable()){GitGenerator.doGenerator(outputPath, meta.getGit().getGitignore());}// ...}
}

5、执行测试,检查代码生成器根目录下是否被 git 托管:
在这里插入图片描述

三、健壮性优化

什么是健壮性?

健壮性是企业开发中至关重要的特性,通常是指程序在不同条件下能够正常运行。一个健壮的程序能够在不同的用户输入和使用方式下,保持正常运行,并且能够正确处理异常情况,而不是整个程序崩溃、出现严重错误。

健壮性优化策略

常见的优化策略有:输入校验、异常处理、故障恢复、自动重试、服务降级等。

对于代码生成器项目,影响代码生成结果的、也是需要用户修改的核心内容是元信息配置文件,所以一定要对元信息进行校验、填充默认值,防止用户错误输入导致的异常,从而提高健壮性。

元信息校验和默认值填充

1、规则梳理

编写代码前,我们对元信息每个字段的校验规则和默认值填充规则作如下梳理:

字段默认值校验规则
namemy-generator
description我的代码生成器
basePackagecom.liucc
version1.0
authorliucc
createTime系统当前日期
git.enablefalse
git.gitignore当enable为true时,必填
fileConfig.sourceRootPath必填
fileConfig.inputRootPath.source+fileConfig.sourceRootPath的最后一层文件路径
fileConfig.outputRootPathgenerated
typedir
fileConfig.files.inputPath必填
fileConfig.files.outputPath等于inputPath(TODO:感觉会有问题,待会儿测试下)
fileConfig.files.typeinputPath 有文件后缀(如 .java)为 file,否则为 dir
fileConfig.files.generateType如果文件结尾不为 .ftl,generateType 默认为 static,否则为 dynamic
modelConfig.models.fieldName必填
modelConfig.models.description
modelConfig.models.typeString
modelConfig.models.defaultValue
modelConfig.models.abbr

2、自定义异常类

由于元信息校验是一个很重要的操作,所以专门定义一个元信息异常类,便于后续集中处理由于元信息输入错误导致的一场。

maker.meta 目录下新增 MetaException.java 文件,代码如下:

public class MetaException extends RuntimeException {public MetaException(String message) {super(message);}public MetaException(String message, Throwable cause) {super(message, cause);}
}

3、编写校验类

由于需要校验的字段比较多,因此我们创建一个单独的校验类MetaValidator,放在maker.meta包下。示例代码如下:

package com.yupi.maker.meta;import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;import java.io.File;
import java.nio.file.Paths;
import java.util.List;/*** 元信息校验*/
public class MetaValidator {public static void doValidAndFill(Meta meta) {// 基础信息校验和默认值String name = meta.getName();if (StrUtil.isBlank(name)) {name = "my-generator";meta.setName(name);}String description = meta.getDescription();if (StrUtil.isEmpty(description)) {description = "我的模板代码生成器";meta.setDescription(description);}String author = meta.getAuthor();if (StrUtil.isEmpty(author)) {author = "yupi";meta.setAuthor(author);}String basePackage = meta.getBasePackage();if (StrUtil.isBlank(basePackage)) {basePackage = "com.yupi";meta.setBasePackage(basePackage);}String version = meta.getVersion();if (StrUtil.isEmpty(version)) {version = "1.0";meta.setVersion(version);}String createTime = meta.getCreateTime();if (StrUtil.isEmpty(createTime)) {createTime = DateUtil.now();meta.setCreateTime(createTime);}// fileConfig 校验和默认值Meta.FileConfig fileConfig = meta.getFileConfig();if (fileConfig != null) {// sourceRootPath:必填String sourceRootPath = fileConfig.getSourceRootPath();if (StrUtil.isBlank(sourceRootPath)) {throw new MetaException("未填写 sourceRootPath");}// inputRootPath:.source + sourceRootPath 的最后一个层级路径String inputRootPath = fileConfig.getInputRootPath();String defaultInputRootPath = ".source" + File.separator + FileUtil.getLastPathEle(Paths.get(sourceRootPath)).getFileName().toString();if (StrUtil.isEmpty(inputRootPath)) {fileConfig.setInputRootPath(defaultInputRootPath);}// outputRootPath:默认为当前路径下的 generatedString outputRootPath = fileConfig.getOutputRootPath();String defaultOutputRootPath = "generated";if (StrUtil.isEmpty(outputRootPath)) {fileConfig.setOutputRootPath(defaultOutputRootPath);}String fileConfigType = fileConfig.getType();String defaultType = "dir";if (StrUtil.isEmpty(fileConfigType)) {fileConfig.setType(defaultType);}// fileInfo 默认值List<Meta.FileConfig.FileInfo> fileInfoList = fileConfig.getFiles();if (CollectionUtil.isNotEmpty(fileInfoList)) {for (Meta.FileConfig.FileInfo fileInfo : fileInfoList) {// inputPath: 必填String inputPath = fileInfo.getInputPath();if (StrUtil.isBlank(inputPath)) {throw new MetaException("未填写 inputPath");}// type:默认 inputPath 有文件后缀(如 .java)为 file,否则为 dirString type = fileInfo.getType();if (StrUtil.isBlank(type)) {// 无文件后缀if (StrUtil.isBlank(FileUtil.getSuffix(inputPath))) {fileInfo.setType("dir");} else {fileInfo.setType("file");}}// generateType:如果文件结尾不为 Ftl,generateType 默认为 static,否则为 dynamicString generateType = fileInfo.getGenerateType();if (StrUtil.isBlank(generateType)) {// 为动态模板if (inputPath.endsWith(".ftl")) {fileInfo.setGenerateType("dynamic");} else {fileInfo.setGenerateType("static");}}// outputPath: generateType如果是static,默认等于 inputPath;// 如果是dynamic,默认为 inputPath 去除掉.ftl后缀的文件路径String outputPath = fileInfo.getOutputPath();if (StrUtil.isEmpty(outputPath)) {if ("static".equals(generateType)){fileInfo.setOutputPath(inputPath);} else {fileInfo.setOutputPath(inputPath.substring(0, inputPath.length() - 4));}}}}}// modelConfig 校验和默认值Meta.ModelConfig modelConfig = meta.getModelConfig();if (modelConfig != null) {List<Meta.ModelConfig.ModelInfo> modelInfoList = modelConfig.getModels();if (CollectionUtil.isNotEmpty(modelInfoList)) {for (Meta.ModelConfig.ModelInfo modelInfo : modelInfoList) {// 输出路径默认值String fieldName = modelInfo.getFieldName();if (StrUtil.isBlank(fieldName)) {throw new MetaException("未填写 fieldName");}String modelInfoType = modelInfo.getType();if (StrUtil.isEmpty(modelInfoType)) {modelInfo.setType("String");}}}}}}

4、圈复杂度优化

上面的代码虽然能够运行,但是过于复杂了,所有的校验规则全写在一起,会导致圈复杂度过高。

圈复杂度(Cyclomatic Complexity)是一种用于评估代码复杂性的软件度量方法。一般情况下,代码的分支判断越多,圈复杂度越高。一般情况下,代码圈复杂度建议 <= 10,不建议超过 20!

在 IDEA 中,可以通过 MetricsReloaded 插件来检测代码圈复杂度,如下图:
在这里插入图片描述

找到需要检测圈复杂度的类或方法(此处是 MetaValidator),点击右键 => 分析 => 计算圈复杂度:
在这里插入图片描述

选择默认即可,直接点击“分析”按钮:
在这里插入图片描述

可以看到圈复杂度严重超标(建议最好不要超过10)
在这里插入图片描述

如何优化圈复杂度呢?

一种方法就是将一段代码拆分为多个方法:

1)抽取方法

我们可以按照元信息配置的层级,将整段代码抽为 3 个方法:基础元信息校验、fileConfig 校验、modelConfig 校验。

在 IDEA 中可以使用 Refactor 重构能力快速抽取

2)在抽取的方法中使用卫语句,尽早放回

卫语句是在进入主要逻辑之前添加的条件检查语句,以确保程序在执行主要逻辑之前满足某些前提条件,这种技术有助于提高代码的可读性和可维护性。

在 IDEA 中可以在 if 代码行上按 Alt + Enter 键,使用 Invert If 选项快速反转 if 条件,如下图:
在这里插入图片描述

3)使用工具类减少判断代码

比如基础元信息校验中,使用 Hutool 的 StrUtil.blankToDefault 代替 if (StrUtil.isBlank(xxx)),这样一行代码就搞定了。示例代码如下:

String name = StrUtil.blankToDefault(meta.getName(), "my-generator");

4)再次检测圈复杂度,发现平均圈复杂度降低了很多倍
在这里插入图片描述

然后就可以在初始化 Meta 对象时,调用校验方法。修改后的 MetaManager 代码如下:

private static Meta initMeta() {String metaJson = ResourceUtil.readUtf8Str("meta.json");Meta newMeta = JSONUtil.toBean(metaJson, Meta.class);// 校验和处理默认值MetaValidator.doValidAndFill(newMeta);return newMeta;
}

四、可扩展性优化

可扩展性是指程序在不修改结构或代码的情况下,能够灵活的添加新功能,并适应新的需求或项目调整。

可扩展性也有很多细分,比如功能可扩展性、性能可扩展性、资源可扩展性及功能可扩展性等,我们开发人员更多可能还是关注功能可扩展性。

1、枚举值定义

我们代码生成器制作工具中存在较多的魔法值,其导致可读性与可维护性较差,可以使用枚举类进行优化,以提高程序的可维护和可扩展。

新建meta.enums包,用于存放元信息相关的枚举类

1)文件类型枚举类:

package com.liucc.marker.meta.enums;public enum FileTypeEnum {DIR("目录", "dir"),FILE("文件", "file");private String name;private String type;FileTypeEnum(String name, String type) {this.name = name;this.type = type;}public String getName() {return name;}public String getType() {return type;}
}

2)生成文件类型枚举类:

package com.liucc.marker.meta.enums;public enum FileGenerateTypeEnum {DYNAMIC("动态", "dynamic"),STATIC("静态", "static");private String name;private String type;FileGenerateTypeEnum(String name, String type) {this.name = name;this.type = type;}public String getName() {return name;}public String getType() {return type;}
}

3)数据模型字段类型枚举类

package com.liucc.marker.meta.enums;public enum ModelFiledTypeEnum {BOOLEAN("布尔", "boolean"),STRING("字符串", "String");private String name;private String type;ModelFiledTypeEnum(String name, String type) {this.name = name;this.type = type;}public String getName() {return name;}public String getType() {return type;}
}

创建meta.constants包,创建元信息相关常量类放在此包下

4)元信息相关常量类

package com.liucc.marker.meta.constants;/*** 元信息常量*/
public interface MetaConstants {String PROJECT_NAME = "my-generator";String PROJECT_DESCRIPTION = "我的模板代码生成器";String PROJECT_AUTHOR = "liucc";String PROJECT_BASEPACKAGE = "com.liucc";String PROJECT_VERSION = "1.0";
}

5)修改MetaValidator元信息校验类,将相关魔法值替换为枚举或常量类,示例代码如下:

package com.liucc.marker.meta;import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import com.liucc.marker.meta.enums.FileGenerateTypeEnum;
import com.liucc.marker.meta.enums.FileTypeEnum;
import com.liucc.marker.meta.enums.ModelFiledTypeEnum;import java.nio.file.Paths;
import java.util.List;import static com.liucc.marker.meta.constants.MetaConstants.*;/*** 元信息校验*/
public class MetaValidator {public static void doValidAndFill(Meta meta) {validAndFillMetaRoot(meta);validAndFillFileConfig(meta);validAndFillModelConfig(meta);}public static void validAndFillModelConfig(Meta meta) {Meta.ModelConfigDTO modelConfig = meta.getModelConfig();if (modelConfig == null) {return;}// modelConfig 默认值List<Meta.ModelConfigDTO.ModelInfo> modelInfoList = modelConfig.getModels();if (!CollectionUtil.isNotEmpty(modelInfoList)) {return;}for (Meta.ModelConfigDTO.ModelInfo modelInfo : modelInfoList) {// 输出路径默认值String fieldName = modelInfo.getFieldName();if (StrUtil.isBlank(fieldName)) {throw new MetaException("未填写 fieldName");}String modelInfoType = modelInfo.getType();if (StrUtil.isEmpty(modelInfoType)) {modelInfo.setType(ModelFiledTypeEnum.STRING.getType());}}}public static void validAndFillFileConfig(Meta meta) {// fileConfig 默认值Meta.FileConfigDTO fileConfig = meta.getFileConfig();if (fileConfig == null) {return;}// sourceRootPath:必填String sourceRootPath = fileConfig.getSourceRootPath();if (StrUtil.isBlank(sourceRootPath)) {throw new MetaException("未填写 sourceRootPath");}// inputRootPath:.source + sourceRootPath 的最后一个层级路径String inputRootPath = fileConfig.getInputRootPath();String defaultInputRootPath = ".source/" + FileUtil.getLastPathEle(Paths.get(sourceRootPath)).getFileName().toString();if (StrUtil.isEmpty(inputRootPath)) {fileConfig.setInputRootPath(defaultInputRootPath);}// outputRootPath:默认为当前路径下的 generatedString outputRootPath = fileConfig.getOutputRootPath();String defaultOutputRootPath = "generated";if (StrUtil.isEmpty(outputRootPath)) {fileConfig.setOutputRootPath(defaultOutputRootPath);}String fileConfigType = fileConfig.getType();String defaultType = FileTypeEnum.DIR.getType();if (StrUtil.isEmpty(fileConfigType)) {fileConfig.setType(defaultType);}// fileInfo 默认值List<Meta.FileConfigDTO.FileInfo> fileInfoList = fileConfig.getFiles();if (!CollectionUtil.isNotEmpty(fileInfoList)) {return;}for (Meta.FileConfigDTO.FileInfo fileInfo : fileInfoList) {// inputPath: 必填String inputPath = fileInfo.getInputPath();if (StrUtil.isBlank(inputPath)) {throw new MetaException("未填写 inputPath");}// outputPath: 默认等于 inputPathString outputPath = fileInfo.getOutputPath();if (StrUtil.isEmpty(outputPath)) {fileInfo.setOutputPath(inputPath);}// type:默认 inputPath 有文件后缀(如 .java)为 file,否则为 dirString type = fileInfo.getType();if (StrUtil.isBlank(type)) {// 无文件后缀if (StrUtil.isBlank(FileUtil.getSuffix(inputPath))) {fileInfo.setType(FileTypeEnum.DIR.getType());} else {fileInfo.setType(FileTypeEnum.FILE.getType());}}// generateType:如果文件结尾不为 Ftl,generateType 默认为 static,否则为 dynamicString generateType = fileInfo.getGenerateType();if (StrUtil.isBlank(generateType)) {// 为动态模板if (inputPath.endsWith(".ftl")) {fileInfo.setGenerateType(FileGenerateTypeEnum.DYNAMIC.getType());} else {fileInfo.setGenerateType(FileGenerateTypeEnum.STATIC.getType());}}}}public static void validAndFillMetaRoot(Meta meta) {// 校验并填充默认值String name = StrUtil.blankToDefault(meta.getName(), PROJECT_NAME);String description = StrUtil.emptyToDefault(meta.getDescription(), PROJECT_DESCRIPTION);String author = StrUtil.emptyToDefault(meta.getAuthor(), PROJECT_AUTHOR);String basePackage = StrUtil.blankToDefault(meta.getBasePackage(), PROJECT_BASEPACKAGE);String version = StrUtil.emptyToDefault(meta.getVersion(), PROJECT_VERSION);String createTime = StrUtil.emptyToDefault(meta.getCreateTime(), DateUtil.now());meta.setName(name);meta.setDescription(description);meta.setAuthor(author);meta.setBasePackage(basePackage);meta.setVersion(version);meta.setCreateTime(createTime);}
}

2、模板方法模式

项目中除了校验类之外,还存在一个实现流程比较复杂的代码 — MainGenerator。这个类的作用是读取元信息配置,根据流程生成不同的代码、执行不同的操作。

所有的流程全部是线性的写在一个方法中,对于不熟悉的开发人员来讲,读这段代码比较费劲。而且后续如果想要在扩展新的流程,需要修改已有的代码,可扩展性比较差。

那么对于有标准流程的代码,有一种方法可以进行优化:那就是模板方法设计模式。

什么是模板方法设计模式?

模板方法模式是通过父类定义一套算法的标准执行流程,再由子类具体实现每个流程的操作。使得子类在不改变执行流程结构的情况下,可以自定义某些步骤的具体实现。

举个例子,假设我们有一个制作咖啡和茶的模板方法。制作过程包括烧水、泡制饮料和添加调料。咖啡和茶在泡制饮料和添加调料的步骤上有所不同,但烧水步骤是相同的。

定义的标准流程有:

1、烧水

2、泡制饮料

3、添加调料


咖啡的具体制作流程是:

1、烧水

2、冲泡咖啡

3、添加糖和牛奶


茶的制作流程

1、烧水

2、泡茶

3、添加柠檬片

这样可以让之类的行为更加规范、复用父类现有的代码,也可以创建新的子类来扩展具体的操作流程,提高了程序的可扩展性。

实现过程

1)流程梳理

MainGenerator的主要执行流程有:复制原始文件、代码生成、使用git托管项目、构建jar包、构建shell脚本及生成精简版代码生成器。

2)创建模板类,定义执行流程

maker.generator.main包下,新建GeneratorTemplate类,定义一个doGenerator方法,将之前MainGenerator的代码逻辑复制到这个doGenerator方法里。

3)方法抽取

根据梳理出来的流程进行方法抽取,将每一个流程抽取成一个单独的方法。使用IDEA自带的抽取方法功能(快捷键ctrl+alt+M
在这里插入图片描述

最终的示例代码如下:

package com.liucc.maker.generator.main;import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.resource.ClassPathResource;
import com.liucc.maker.generator.GitGenerator;
import com.liucc.maker.generator.JarGenerator;
import com.liucc.maker.generator.ScriptGenerator;
import com.liucc.maker.generator.file.DynamicFileGenerator;
import com.liucc.maker.meta.Meta;
import com.liucc.maker.meta.MetaManager;
import freemarker.template.TemplateException;import java.io.File;
import java.io.IOException;public abstract class GeneratorTemplate  {public void doGenerate() throws TemplateException, IOException, InterruptedException  {Meta meta = MetaManager.getMetaObject();String projectPath = System.getProperty("user.dir");String outputPath = projectPath + File.separator + "generated" + File.separator +meta.getName();// 生成目标项目的路径// 路径不存在,则创建if (!FileUtil.exist(outputPath)) {FileUtil.mkdir(outputPath);}// 1、拷贝原始模板文件String sourceCopyDestPath = copySourceFiles(meta, outputPath);// 2、生成代码文件generateCode(meta, outputPath);// 3、使用git托管项目gitProject(meta.getGit(), outputPath);// 4、构建jar包String jarPath = buildJar(outputPath, meta);// 5、构建shell脚本buildShell(outputPath, jarPath);// 6、生成精简版代码生成器generateDist(sourceCopyDestPath, outputPath, jarPath);}protected void generateDist(String sourceCopyDestPath, String outputPath, String jarPath) {// 生成精简版的代码生成器(仅保留 原始模板文件、jar 包、脚本文件)// - 原始模板文件FileUtil.copy(sourceCopyDestPath, outputPath + "-dist", true);// - jar 包String jarCopySourcePath = outputPath + File.separator + jarPath;String jarCopyDestPath = outputPath + "-dist" + File.separator + jarPath;FileUtil.copy(jarCopySourcePath, jarCopyDestPath, true);// - 脚本文件String shellCopySourcePath = outputPath + File.separator + "generator";String shellCopyDestPath = outputPath + "-dist";FileUtil.copy(shellCopySourcePath, shellCopyDestPath, true);shellCopySourcePath = outputPath + File.separator + "generator.bat";FileUtil.copy(shellCopySourcePath, shellCopyDestPath, true);}protected void buildShell(String outputPath, String jarPath) throws IOException {// 封装脚本String shellOutputFilePath = outputPath + File.separator + "generator";ScriptGenerator.doGenerate(shellOutputFilePath, jarPath);}/*** 构建jar包* @param outputPath* @param meta* @return 返回jar包所在路径* @throws IOException* @throws InterruptedException*/protected String buildJar(String outputPath, Meta meta) throws IOException, InterruptedException {// 构建jar包JarGenerator.doGenerate(outputPath);String jarName = String.format("%s-%s-jar-with-dependencies.jar", meta.getName(), meta.getVersion());String jarPath = "target/" + jarName;return jarPath;}protected void gitProject(Meta.Git git, String outputPath) {// 使用 git 托管项目if (git.getEnable()){GitGenerator.doGenerator(outputPath, git.getGitignore());}}protected void generateCode(Meta meta, String outputPath) throws IOException, TemplateException {// 获取 resources 目录ClassPathResource classPathResource = new ClassPathResource("");String inputResourcePath = classPathResource.getAbsolutePath();// Java包基础路径// com.liuccString outputBasePackage = meta.getBasePackage();// 转为 com/liuccString outputBasePackagePath = outputBasePackage.replaceAll("\\.", "/");String outputBaseJavaPackagePath = outputPath + File.separator + "src/main/java/" + outputBasePackagePath;String inputFilePath;String outputFilePath;// 生成数据模型文件inputFilePath = inputResourcePath + File.separator + "templates/java/model/DataModel.java.ftl";outputFilePath = outputBaseJavaPackagePath + File.separator + "model/DataModel.java";DynamicFileGenerator.doGenerate(inputFilePath, outputFilePath, meta);// 生成Picocli 命令类文件// cli.command.ConfigCommandinputFilePath = inputResourcePath + File.separator + "templates/java/cli/command/ConfigCommand.java.ftl";outputFilePath = outputBaseJavaPackagePath + File.separator + "cli/command/ConfigCommand.java";DynamicFileGenerator.doGenerate(inputFilePath, outputFilePath, meta);// cli.command.GenerateCommandinputFilePath = inputResourcePath + File.separator + "templates/java/cli/command/GenerateCommand.java.ftl";outputFilePath = outputBaseJavaPackagePath + File.separator + "cli/command/GenerateCommand.java";DynamicFileGenerator.doGenerate(inputFilePath, outputFilePath, meta);// cli.command.ListCommandinputFilePath = inputResourcePath + File.separator + "templates/java/cli/command/ListCommand.java.ftl";outputFilePath = outputBaseJavaPackagePath + File.separator + "cli/command/ListCommand.java";DynamicFileGenerator.doGenerate(inputFilePath, outputFilePath, meta);// cli.CommandExecutorinputFilePath = inputResourcePath + File.separator + "templates/java/cli/CommandExecutor.java.ftl";outputFilePath = outputBaseJavaPackagePath + File.separator + "cli/CommandExecutor.java";DynamicFileGenerator.doGenerate(inputFilePath, outputFilePath, meta);// java.MaininputFilePath = inputResourcePath + File.separator + "templates/java/Main.java.ftl";outputFilePath = outputBaseJavaPackagePath + File.separator + "Main.java";DynamicFileGenerator.doGenerate(inputFilePath, outputFilePath, meta);// MainGenerator.javainputFilePath = inputResourcePath + File.separator + "templates/java/generator/MainGenerator.java.ftl";outputFilePath = outputBaseJavaPackagePath + File.separator + "generator/MainGenerator.java";DynamicFileGenerator.doGenerate(inputFilePath, outputFilePath, meta);// DynamicGenerator.javainputFilePath = inputResourcePath + File.separator + "templates/java/generator/DynamicGenerator.java.ftl";outputFilePath = outputBaseJavaPackagePath + File.separator + "generator/DynamicGenerator.java";DynamicFileGenerator.doGenerate(inputFilePath, outputFilePath, meta);// StaticGenerator.javainputFilePath = inputResourcePath + File.separator + "templates/java/generator/StaticGenerator.java.ftl";outputFilePath = outputBaseJavaPackagePath + File.separator + "generator/StaticGenerator.java";DynamicFileGenerator.doGenerate(inputFilePath, outputFilePath, meta);// pom.xmlinputFilePath = inputResourcePath + File.separator + "templates/pom.xml.ftl";outputFilePath = outputPath + File.separator + "pom.xml";DynamicFileGenerator.doGenerate(inputFilePath, outputFilePath, meta);// README.mdinputFilePath = inputResourcePath + File.separator + "templates/README.md.ftl";outputFilePath = outputPath + File.separator + "README.md";DynamicFileGenerator.doGenerate(inputFilePath, outputFilePath, meta);}protected String copySourceFiles(Meta meta, String outputPath) {// 将模板项目 copy 到.source目录下String sourcePath = meta.getFileConfig().getSourceRootPath();String sourceCopyDestPath = outputPath + File.separator + ".source";FileUtil.copy(sourcePath, sourceCopyDestPath, true);return sourceCopyDestPath;}
}

4)编写模板方法的具体实现子类

已经有了模板类,现在我们让MainGenerator继承GeneratorTemplate类。假设我们现在这样一个需求:我不需要生成精简版得代码,那么就可以在MainGenerator类中重写generateDist方法,而不是直接去父类中修改原先的代码,这样程序就具备足够的灵活性和可扩展性。示例代码如下:

package com.liucc.maker.generator.main;import freemarker.template.TemplateException;import java.io.IOException;public class MainGenerator extends GeneratorTemplate {/*** 扩展父类生成简化版 方法* @param sourceCopyDestPath* @param outputPath* @param jarPath*/@Overrideprotected void generateDist(String sourceCopyDestPath, String outputPath, String jarPath) {System.out.println("我不需要精简版代码,不要给我生成啦~~~");}
}

5)调用生成器

marker.Main主类中,调用MainGeneratordoGenerate方法来生成代码生成器,检查最终的生成文件中是否还存在精简版生成器:
在这里插入图片描述

主类Main的完整代码如下:

package com.liucc.maker;import com.liucc.maker.generator.main.MainGenerator;
import freemarker.template.TemplateException;import java.io.IOException;public class Main {public static void main(String[] args) throws TemplateException, IOException, InterruptedException {MainGenerator generator = new MainGenerator();generator.doGenerate();}
}

最后

我们在本章节中,从可移植性、功能优化、健壮性及功能可扩展性等四个角度对现有的代码生成器项目进行了优化。在以后的工作项目中,完成了一个需求后,也要时常考虑自己的代码是否还可以从这几个方面再进行优化。

本期作业

1)掌握几种常见的项目优化方式,并尝试优化自己之前做过的项目

2)掌握圈复杂度优化、模板方法模式等重要知识点

3)自己编写代码实现本节项目,并且在自己的代码仓库完成一次提交

相关文章:

  • VUE简介
  • 【 图像梯度处理,图像边缘检测】图像处理(OpenCv)-part6
  • C++(17):通过filesystem获取文件的大小
  • electron 渲染进程按钮创建新window,报BrowserWindow is not a constructor错误;
  • 【go】什么是Go语言的GPM模型?工作流程?为什么Go语言中的GMP模型需要有P?
  • 好数对的数目
  • MySQL事务详解
  • C#如何动态生成实体类?5种方法详解与实战演示
  • 《TIME-LLM: TIME SERIES FORECASTINGBY REPROGRAMMING LARGE LANGUAGE MODELS》
  • 51单片机实验三:数码管动态显示
  • 游戏引擎学习第233天
  • 基于Redis的4种延时队列实现方式
  • AI数据分析与BI可视化结合:解锁企业决策新境界
  • HTML新标签与核心 API 实战
  • 杂记-LeetCode中部分题思路详解与笔记-HOT100篇-其四
  • LVGL学习(二)——控件
  • ArcPy工具箱制作(下)
  • 【Hot100】41. 缺失的第一个正数
  • 轻量还是全量?Kubernetes ConfigMap 与专业配置中心的抉择
  • 每日一题(8) 求解矩阵最小路径和问题
  • 今年1-3月全国吸收外资2692.3亿元人民币
  • 科技如何赋能社会治理?帮外卖员找新家、无人机处理交通事故……
  • 商务部:敦促美方立即停止极限施压,停止胁迫讹诈
  • 浙江队确认外援布彭扎不幸去世,向其家人致以沉痛的哀悼
  • 律师详解中国留学生诉美国政府:撤签程序违规,拟争取全美禁止令
  • 12家券商一季度业绩报喜:国泰海通净利规模暂列第一,东北证券预增859%