前言

  • 在软件开发中,往往会遇到多个功能模块存在重复的流程或逻辑,但它们的业务细节又有所不同。为避免代码冗余,提升代码的复用性和可维护性,设计模式等方法提供了很好的解决方案。通过识别和提取通用逻辑,开发者可以将相似的代码抽象为更加灵活和扩展性强的结构,适用于不同场景的需求。
  • 本文将展示如何从重复的业务代码中提取共性,逐步优化并提升代码的可维护性与灵活性,最终形成一个更加通用、扩展性强的解决方案。
  • 不必关心我这里的业务逻辑,仅仅关心模板的提取。

现状分析:重复代码的问题

  • 在原有实现中,每个文档生成 API 都包含相同的文件下载处理逻辑,如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@PostMapping("/generateReferenceReview")
public void generateReferenceReview(String json, String orderId, HttpServletResponse response) throws Exception {
// 这部分内容是文档的生成逻辑
String diskPath = orderConsumerService.generateReferenceReview(JSONObject.parseObject(json));

// 这部分内容是重复的
CommonUtil.setContentType(response, diskPath);
response.setHeader("Content-Disposition", "attachment;filename=" +
java.net.URLEncoder.encode(FilenameUtils.getName(diskPath), "UTF-8").replaceAll("\\+", "%20")
);
ServletOutputStream outputStream = response.getOutputStream();
FileUtils.copyFile(new File(diskPath), outputStream);
outputStream.close();
}

@PostMapping("/generatePPT")
public void generatePPT(String json, String orderId, HttpServletResponse response) throws Exception {
String diskPath = orderConsumerService.generatePPT(JSONObject.parseObject(json), orderId);

CommonUtil.setContentType(response, diskPath);
response.setHeader("Content-Disposition", "attachment;filename=" +
java.net.URLEncoder.encode(FilenameUtils.getName(diskPath), "UTF-8").replaceAll("\\+", "%20")
);
ServletOutputStream outputStream = response.getOutputStream();
FileUtils.copyFile(new File(diskPath), outputStream);
outputStream.close();
}
  • 即使抽取公共代码作为独立方法,依然会存在重复的逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@PostMapping("/generateReferenceReview")
public void generateReferenceReview(String json, String orderId, HttpServletResponse response) throws Exception {
String diskPath = orderConsumerService.generateReferenceReview(JSONObject.parseObject(json));

// 但是这里还是重复的,只不过变成了一行调用,治标不治本
handleFileResponse(response, diskPath);
}

@PostMapping("/generatePPT")
public void generatePPT(String json, String orderId, HttpServletResponse response) throws Exception {
String diskPath = orderConsumerService.generatePPT(JSONObject.parseObject(json), orderId);
handleFileResponse(response, diskPath);
}

private static void handleFileResponse(HttpServletResponse response, String diskPath) throws IOException {
CommonUtil.setContentType(response, diskPath);
response.setHeader("Content-Disposition", "attachment;filename=" +
java.net.URLEncoder.encode(FilenameUtils.getName(diskPath), "UTF-8").replaceAll("\\+", "%20")
);
ServletOutputStream outputStream = response.getOutputStream();
FileUtils.copyFile(new File(diskPath), outputStream);
outputStream.close();
}
  • 存在的问题:
    1. 代码重复:每个 API 都要写相同的文件下载逻辑,违反DRY(Don’t Repeat Yourself)原则。
    2. 扩展性差:如果需要修改文件下载方式,所有 API 代码都需要调整。
    3. 维护成本高:新增文档类型时,需要复制粘贴代码,容易出现不一致性问题。

使用模板方法模式优化代码

什么是模板方法模式?

  • 模板方法模式(Template Method Pattern)是一种行为设计模式,它定义一个算法的通用流程,并允许子类或外部方法提供具体实现。它的核心思想是将通用逻辑抽取到父类(或公共组件)中,让子类(或具体方法)实现可变部分。

代码优化:引入 DocGenerateExecutor

  • 为了避免继承带来的复杂性,我采用了 策略模式 + 函数式接口 的方式,避免传统模板方法模式的局限性。核心思想是将文件下载逻辑封装到一个可复用的执行器 DocGenerateExecutor 中,并通过函数式接口让不同业务代码提供具体的文档生成逻辑。
    1. 创建 DocGenerateExecutor 统一处理文档下载
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @Component
    public class DocGenerateExecutor {
    // 定义函数式接口,让外部提供具体文档生成逻辑
    public interface DocGenerateFun {
    String executor() throws Exception;
    }

    // 通用的文档生成和下载逻辑
    public void generateDoc(DocGenerateFun fun, HttpServletResponse response) throws Exception {
    String diskPath = fun.executor();
    CommonUtil.setContentType(response, diskPath);
    response.setHeader("Content-Disposition", "attachment;filename=" +
    java.net.URLEncoder.encode(FilenameUtils.getName(diskPath), "UTF-8").replaceAll("\\+", "%20")
    );
    ServletOutputStream outputStream = response.getOutputStream();
    FileUtils.copyFile(new File(diskPath), outputStream);
    outputStream.close();
    }
    }
    1. 调用 DocGenerateExecutor 处理文档下载
    1
    2
    3
    4
    @PostMapping("/generateOpenReport")
    public void generateOpenReport(String json, String orderId, HttpServletResponse response) throws Exception {
    docGenerateExecutor.generateDoc(() -> orderConsumerService.generateOpenReport(JSONObject.parseObject(json), orderId), response);
    }

代码优化的效果与优势

优化后代码的优势

优化点 传统实现 优化后实现
代码复用性 每个 API 复制相同的下载逻辑 只需调用 DocGenerateExecutor
扩展性 需要修改多个 API 代码 只需修改 DocGenerateExecutor
可维护性 代码冗余,容易引入 Bug 代码简洁,降低维护成本

代码行数减少

  1. 传统代码需要在每个 API 里写一遍文件下载逻辑,而优化后只需调用 generateDoc(),减少了大量重复代码。

  2. 如果需要调整下载逻辑,比如支持流式下载、日志记录等,只需修改 DocGenerateExecutor 一处,所有 API 无需修改。

适配不同的文档生成逻辑

  1. 只需要传入不同的 Lambda 表达式,即可适配不同的文档生成逻辑。
  2. 无须继承,避免 Java 传统模板方法模式的类爆炸问题。