Java Spring Boot实现PDF服务应复制代码还是用模板方法模式?
结论
这个场景非常适合用模板方法模式做抽象重构,直接复制代码的写法短期看省时间,长期维护的坑非常多。
直接复制代码的问题
你现在两个实现类里重复的逻辑占了80%以上:
- 创建渲染上下文
Context的逻辑重复 - 调用模板引擎渲染返回的逻辑重复
- 填充产品信息
setProductInformationToHtml的逻辑重复
后续只要你需要改公共逻辑——比如统一加PDF生成时间、页脚版权参数、全局渲染规则,就得挨个改所有实现类,漏改一个就出线上bug。等你后续再加3、4种不同类型的PDF生成服务,维护成本会指数级上升。
重构方案(贴合Spring Boot场景)
模板方法的核心就是把固定的流程骨架抽到抽象基类,可变的业务差异部分交给子类实现,刚好匹配你现在的代码结构:
- 先写抽象基类,把固定的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逻辑移到这里 } }
- 改造后的业务实现类会非常简洁,没有任何重复代码,只保留自己的差异化逻辑:
@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
相关产品推荐
相关产品推荐

