You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java Spring Boot实现PDF服务应复制代码还是用模板方法模式?

结论

这个场景非常适合用模板方法模式做抽象重构,直接复制代码的写法短期看省时间,长期维护的坑非常多。

直接复制代码的问题

你现在两个实现类里重复的逻辑占了80%以上:

  • 创建渲染上下文Context的逻辑重复
  • 调用模板引擎渲染返回的逻辑重复
  • 填充产品信息setProductInformationToHtml的逻辑重复
    后续只要你需要改公共逻辑——比如统一加PDF生成时间、页脚版权参数、全局渲染规则,就得挨个改所有实现类,漏改一个就出线上bug。等你后续再加3、4种不同类型的PDF生成服务,维护成本会指数级上升。

重构方案(贴合Spring Boot场景)

模板方法的核心就是把固定的流程骨架抽到抽象基类,可变的业务差异部分交给子类实现,刚好匹配你现在的代码结构:

  1. 先写抽象基类,把固定的PDF生成流程锁死,把差异点抽成子类可实现的扩展点
// 基类统一管理PDF生成的固定流程
public abstract class AbstractPDFService<T> implements PDFService {

    // 模板引擎直接在基类注入,不用每个子类重复声明
    protected final TemplateEngine templateEngine;

    protected AbstractPDFService(TemplateEngine templateEngine) {
        this.templateEngine = templateEngine;
    }

    // 加final禁止子类重写整个生成流程,避免流程被打乱
    @Override
    public final String generatePdf(UUID uuid) {
        Context context = new Context();
        T bizDto = queryBizDto(uuid);
        fillCustomContext(context, bizDto);
        // 公共的产品信息填充逻辑直接移到基类,所有子类复用
        fillProductInfo(context, bizDto);
        return templateEngine.process(getTemplatePath(), context);
    }

    // 扩展点1:子类实现,根据uuid查询自己的业务DTO
    protected abstract T queryBizDto(UUID uuid);

    // 扩展点2:子类实现,填充自己业务专属的模板变量
    protected abstract void fillCustomContext(Context context, T bizDto);

    // 扩展点3:返回当前实现对应的模板路径,给默认值,有特殊需求的子类重写即可
    protected String getTemplatePath() {
        return "cookbookTemplate";
    }

    // 公共逻辑:填充产品信息,建议给BrandDTO、CookbookDTO抽个公共的产品信息接口,这里直接基于接口取值就行,不用写类型判断
    private void fillProductInfo(Context context, T bizDto) {
        // 把原来两个类里重复的setProductInformationToHtml逻辑移到这里
    }
}
  1. 改造后的业务实现类会非常简洁,没有任何重复代码,只保留自己的差异化逻辑:
@Service
public class BrandPDFServiceImpl extends AbstractPDFService<BrandDTO> {

    private final BrandService brandService;

    public BrandPDFServiceImpl(TemplateEngine templateEngine, BrandService brandService) {
        super(templateEngine);
        this.brandService = brandService;
    }

    @Override
    protected BrandDTO queryBizDto(UUID uuid) {
        return brandService.findByUuid(uuid);
    }

    @Override
    protected void fillCustomContext(Context context, BrandDTO dto) {
        context.setVariable("brandName", dto.getName());
        context.setVariable("brandLogoUrl", dto.getImageUrl());
    }
}
@Service
public class CookbookPDFServiceImpl extends AbstractPDFService<CookbookDTO> {

    private final CookbookService cookbookService;

    public CookbookPDFServiceImpl(TemplateEngine templateEngine, CookbookService cookbookService) {
        super(templateEngine);
        this.cookbookService = cookbookService;
    }

    @Override
    protected CookbookDTO queryBizDto(UUID uuid) {
        return cookbookService.findByUuid(uuid);
    }

    @Override
    protected void fillCustomContext(Context context, CookbookDTO dto) {
        context.setVariable("cookbookName", dto.getName());
        context.setVariable("cookbookLogoUrl", dto.getImageUrl());
    }
}

什么时候可以不用重构?

如果你100%确定后续不会新增第三种PDF生成类型,也永远不会修改PDF生成的公共逻辑,那复制代码也能跑。但只要有一点扩展或者调整公共逻辑的可能,这个重构的投入产出比极高,花10分钟改完,后续能省几小时的重复改代码、排错的时间。

另外顺手提个小问题:你最开始定义的PDFService<T, S>接口声明了两个泛型参数,但现有代码完全没用到,重构的时候可以顺手把泛型定义调整成和实际使用匹配的形式,避免后续类型强转的问题。

内容的提问来源于stack exchange,提问作者user19254373

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 15:06:21