前言

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

原始代码

  • 在实际项目中,我遇到的场景是不同的功能模块具有相似的处理流程。例如,在生成各种文档时,每种文档的生成逻辑各自独立,但处理文件输出和响应的部分却高度重复。如下所示:
    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 来处理所有类似的流程,业务逻辑只需专注于各自的实现。这不仅减少了重复代码,还使得框架更加灵活、可扩展。

总结

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