前言

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

原始代码

  • 在实际项目中,我遇到的场景是不同的功能模块具有相似的处理流程。例如,在生成各种文档时,每种文档的生成逻辑各自独立,但处理文件输出和响应的部分却高度重复。如下所示:
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();
}
  • 在这段代码中,尽管 generateReferenceReviewgeneratePPT 方法在业务逻辑上不同,但文件的处理和响应逻辑是完全一致的。显然,这种重复的代码增加了维护成本,也违背了 DRY(Don’t Repeat Yourself)原则。因此,首先我们可以通过提取公共代码来减少冗余。

优化代码

  • 通过将重复的响应逻辑抽象为一个通用方法,可以减少代码冗余,提升可读性:
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();
}

使用模板模式简化

  • 为了提升代码复用性和扩展性,可以使用模板方法模式,将业务逻辑和通用的流程处理分离开来。我们定义一个执行器类 DocGenerateExecutor 来封装通用的流程,业务逻辑则通过函数式接口传递给执行器。这样可以在不同场景下复用相同的流程,且保证扩展性。
  • 通过模板方法模式,将业务逻辑与通用的流程代码解耦。首先,定义一个通用的任务执行器 DocGenerateExecutor 类,它负责执行具体业务逻辑并处理通用的后续逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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();
}
}
  • 业务逻辑则通过 DocGenerateFun 接口传递给 DocGenerateExecutor,不同的任务只需实现其业务逻辑,无需关心通用流程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Slf4j
@RestController
@RequestMapping("/doc")
public class DocController {

@Resource
private DocGenerateExecutor docGenerateExecutor;

@PostMapping("/generateOpenReport")
public void generateOpenReport(String json, String orderId, HttpServletResponse response) throws Exception {
OpenReport openReport = openReportMapper.selectOne(Wrappers.lambdaQuery(OpenReport.class).eq(OpenReport::getOrderId, orderId));

// 现在只需要一行处理了
docGenerateExecutor.generateDoc(() -> orderConsumerService.generateOpenReport(JSONObject.parseObject(json), openReport.getEdu(), orderId), response);
}

@PostMapping("/generateOpenReportPPT")
public void generateOpenReportPPT(String json, String orderId, HttpServletResponse response) throws Exception {
docGenerateExecutor.generateDoc(() -> orderConsumerService.generateOpenReportPPT(JSONObject.parseObject(json), orderId), response);
}
}
  • 在此优化中,DocGenerateExecutor 提供了一个统一的模板方法 generateDoc 来处理所有类似的流程,业务逻辑只需专注于各自的实现。这不仅减少了重复代码,还使得框架更加灵活、可扩展。

总结

  • 通过本文的示例,我们展示了如何从重复代码中提取共性,使用模板方法模式将通用逻辑与业务逻辑解耦,提升代码的复用性与可维护性。采用这种设计思路,能够显著减少代码的冗余,提升系统的扩展性和灵活性。