重构与代码生成:重构重复的样板代码

样板代码,说白了就是那些你写了无数遍、结构几乎一模一样、只是换了个业务字段的代码。比如 CRUD 的 Service 层、DTO 转换、配置类注册……我见过太多项目,一半以上的代码都是这种「复制粘贴改改字段」的活儿。

嗯,今天我们就聊聊怎么用代码生成来干掉这些重复劳动。这不是让你偷懒,而是让你把精力花在真正有挑战的地方。

样板代码到底有多「毒」?

先说说我自己的经历。几年前我接手过一个老项目,光是一个实体类的增删改查,就写了 2000 多行。四个模块加起来,类似的代码重复了 6 遍。每次加个字段,要改 6 个地方。有一次漏改了一个查询条件,线上数据直接查不全。

为什么会这样?因为人不是机器,重复的事情做多了,一定会出错。

样板代码的典型问题:

  • 维护成本高:改一处逻辑,要同步 N 个地方
  • 容易遗漏:复制粘贴时漏改一个变量名,就是 bug
  • 代码膨胀:项目 70% 的代码都是「废话」,真正业务逻辑被淹没
  • 新人上手慢:满屏的重复代码,根本分不清哪些是核心逻辑

我个人习惯,当一段代码出现第三次的时候,就该想想能不能用生成器解决了。

代码生成的三种常见模式

代码生成不是非得搞个高大上的脚手架。根据场景不同,我总结了三种模式,你可以按需选用。

模式 适用场景 典型工具 生成时机
模板引擎生成 固定结构的代码(如 DTO、Mapper) FreeMarker、Velocity 编译前 / 构建时
注解处理器生成 编译期自动生成(如 Lombok、MapStruct) Annotation Processor 编译时
运行时动态生成 需要动态扩展的场景(如规则引擎) ByteBuddy、CGLIB 运行时

我个人最常用的是第一种——模板引擎生成。它最直观,也最容易控制。你想想看,一个模板文件写好,输入不同的数据模型,就能输出不同的 Service、Controller、DTO。这不就是「一次编写,到处生成」吗?

实战:用模板引擎干掉重复的 CRUD

假设我们有一个典型的 Spring Boot 项目,每个实体都需要一个 Service 接口、一个 Service 实现、一个 Controller。这些代码的结构几乎一模一样,只是实体名和字段不同。

我们先定义一个 FreeMarker 模板,比如 Service.ftl

package ${packageName}.service;

import ${packageName}.entity.${entityName};
import java.util.List;

public interface ${entityName}Service {
    List<${entityName}> findAll();
    ${entityName} findById(Long id);
    ${entityName} save(${entityName} entity);
    void deleteById(Long id);
}

然后写一个生成器类,读取实体信息,填充模板:

public class CodeGenerator {
    public static void main(String[] args) throws Exception {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_31);
        cfg.setClassForTemplateLoading(CodeGenerator.class, "/templates");
        cfg.setDefaultEncoding("UTF-8");

        // 假设我们从数据库或配置文件读取了实体列表
        List<EntityMeta> entities = loadEntities();

        for (EntityMeta entity : entities) {
            Map<String, Object> data = new HashMap<>();
            data.put("packageName", "com.example.demo");
            data.put("entityName", entity.getName());

            Template template = cfg.getTemplate("Service.ftl");
            File output = new File("output/" + entity.getName() + "Service.java");
            template.process(data, new FileWriter(output));
        }
    }
}

你看,核心逻辑就这么几行。每次新增一个实体,只需要在配置里加一行,运行一下生成器,所有样板代码就自动生成了。

小技巧: 我建议把生成器做成 Maven 插件或 Gradle Task,这样每次构建时自动执行,保证生成的代码和实体定义始终同步。

重构视角:生成代码 vs 手写代码

有人可能会问:「生成的代码质量能保证吗?会不会比手写的差?」

我的回答是:生成的代码应该比你手写的更规范。因为模板是经过反复打磨的,所有边界情况都考虑到了。而手写的时候,人很容易偷懒——少写一个 null 判断、漏掉一个事务注解,这些都是常见问题。

我曾经在一个项目中,把所有 Controller 的异常处理都写进了模板。结果生成出来的代码,每个接口都有统一的 try-catch 和日志记录。后来审计代码的时候,发现所有接口的异常处理覆盖率是 100%。如果是手写,我敢说至少有一半的接口会漏掉。

注意: 生成的代码不要直接修改!如果你改了生成的文件,下次重新生成时修改就丢了。正确的做法是:把生成的代码当作「基础代码」,自定义逻辑通过继承、组合或扩展点来实现。

知识体系:样板代码重构的核心逻辑

下面这张图是我自己总结的「样板代码重构决策流程」,帮你快速判断什么时候该用代码生成。

样板代码重构决策流程 发现重复代码 重复次数 >= 3? 结构固定? 使用模板生成 先手动重构 提取抽象层 代码量减少 50%+

这个流程的核心思想很简单:先判断重复次数,再判断结构是否固定。如果重复次数少,手动重构就够了;如果结构不固定,那就需要提取抽象层,而不是硬套模板。

避坑指南:我曾经踩过的三个坑

代码生成不是银弹,用不好反而会引入新问题。我把自己踩过的坑分享给你,希望能帮你少走弯路。

  • 坑一:模板过于灵活。我曾经设计了一个「万能模板」,支持各种条件分支、循环嵌套。结果模板本身比手写代码还难维护。后来我学乖了:一个模板只做一件事,宁可多写几个模板,也不搞大而全。
  • 坑二:生成代码和手写代码混在一起。有一次我生成了 Service 层,但手写了一些自定义方法。结果重新生成时,手写代码被覆盖了。解决方案:生成代码放在单独的包或模块中,手写代码通过继承或委托来扩展。
  • 坑三:忽略测试。生成的代码也是代码,同样需要测试。我建议为生成器本身写单元测试,确保模板输出符合预期。另外,生成后的代码也要跑一遍集成测试,防止模板更新引入回归。

核心原则: 代码生成的目标是消除重复,而不是制造新的重复。如果生成器本身需要大量维护,那说明你选错了方向。

总结

样板代码重构,说白了就是「把重复交给机器,把创造留给自己」。我个人习惯在项目初期就搭建好代码生成的基础设施,这样后续新增功能时,只需要关注业务逻辑,而不是在复制粘贴中消耗精力。

记住:好的代码生成器,应该像一把锋利的刀——切掉冗余,留下精华。


公众号:蓝海资料掘金营,微信deep3321