还在重复造轮子?模板模式教你如何用20%代码解决80%业务难题

作者:佚名 时间:2025-11-17 13:42

字号

今天我们来学习下模板模式,以及在项目中的应用

01 模板模式简介

模板模式(Template Pattern)是一种行为设计模式,它定义了一个操作中的算法骨架,将某些步骤延迟到子类中实现。模板模式使得子类可以不改变算法结构的情况下,重新定义算法中的某些特定步骤。

主要角色

02 基础代码实现

以泡茶为例,整个流程可以简化为三个步骤 烧开水、放茶叶、倒入开水,我们将放茶叶定义为可变的部分,比如有人喜欢绿茶、有人喜欢红茶、有人喜欢玫瑰花茶。

首先,创建一个抽象模板,定义泡茶方法:

/**
 * @description 泡茶抽象模板
 */
public abstract class AbstractMakeTeaTemplate {
    /**
     * 泡茶
     */
    public final void makeTea() {
        // 1. 烧开水
        boilWater();
        // 2. 放入茶叶
        putTea();
        // 3. 加入水
        addWater();
    }
    private void boilWater() {
        System.out.println("烧开水");
    }
    /**
     * 放入茶叶
     */
    protected abstract void putTea();
    private void addWater() {
        System.out.println("加入水");
    }
}

makeTea() 方法就是泡茶方法,烧开水、添加开水是必需的步骤,唯有放入茶叶是可变的,下面我们分别实现使用不同茶叶泡茶的业务。

/**
 * @description 绿茶
 */
public class GreenTea extends AbstractMakeTeaTemplate {
    @Override
    protected void putTea() {
        System.out.println("放入龙井茶叶");
    }
}

/**
 * @description 红茶
 */
public class RedTea extends AbstractMakeTeaTemplate {
    @Override
    protected void putTea() {
        System.out.println("放入普尔茶叶");
    }
}

/**
 * @description 玫瑰茶
 */
public class RoseTea extends AbstractMakeTeaTemplate {
    @Override
    protected void putTea() {
        System.out.println("放入玫瑰花茶叶");
    }
}

使用main方法测试下:

public static void main(String[] args) {
    // 1. 制作绿茶
    AbstractMakeTeaTemplate greenTea = new GreenTea();
    greenTea.makeTea();
}

输出结果如下:

烧开水
放入绿茶
加入水

由此,我们通过模板方式,封装了一个泡茶的流程,但对于其中可变的部分放到子类去实现。后续如果有新业务到来,比如要泡其他茶叶,只需要实现泡茶模板,重写茶叶相关的方法即可。

03 项目实战

以上只是一个简单的小demo,真实业务中很少会有泡茶的情况。项目实战中我们以数据导出为例,看下导出模板的设计和实现(数据导出借助easy-excel完成)。首先定义导出流程:参数校验-》读取数据-》导出-》导出后逻辑处理

抽象模板:

/**
 * @description 数据导出模板
 */
public abstract class AbstractExportTemplate {
    /**
     * 导出数据
     * 

声明为final类型,防止子类重写,确保导出流程的一致性

*/
public final void export(BaseExportParam

baseExportParam) { // 1. 校验参数 validateParams(baseExportParam.getParams()); // 2. 读取数据 List data = readData(baseExportParam.getParams()); // 3. 导出数据 doExport(baseExportParam, data); // 4. 导出完成后处理 afterExport(); } /** * 获取导出实体的Class对象 *

子类必须实现此方法提供具体的实体类型

*/
protected abstract Class getEntityClass(); /** * 读取数据 *

声明为abstract类型,强制要求子类实现,确保数据读取的一致性

*/
protected abstract List readData(P params); /** * 校验参数 *

子类根据自身业务情况,看是否需要进行参数校验

*/
protected void validateParams(P params) { } /** * 导出数据 */ private void doExport(BaseExportParam

baseExportParam, List data) { HttpServletResponse response = baseExportParam.getResponse(); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); // 这里URLEncoder.encode可以防止中文乱码 String fileName = URLEncoder.encode(baseExportParam.getFileName() + "_" + System.currentTimeMillis(), StandardCharsets.UTF_8); response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx"); try { EasyExcelFactory.write(response.getOutputStream(), getEntityClass()) .sheet(baseExportParam.getFileName()) .doWrite(data); } catch (Exception e) { throw new RuntimeException("导出数据失败", e); } } protected void afterExport() { // 导出完成后处理逻辑(可空) } }

模板入参基类:

/**
 * @description 导出基类
 */
@Getter
public class BaseExportParam

{ /** * 导出参数 */ protected P params; /** * 导出文件名 */ protected String fileName; /** * 导出响应 */ protected HttpServletResponse response; public BaseExportParam(P params, String fileName, HttpServletResponse response) { this.params = params; this.fileName = fileName; this.response = response; } }

可以看到导出的流程方法是export,默认添加了final修饰,防止子类重写从而改变导出流程定义。真正执行的导出的方法是doExport。子类需要实现getEntityClass、readData方法,另外两个方法(validateParams、afterExport)模板类已经做了默认实现,子类视情况选择是否实现。

用户导出

/**
 * @description 用户导出查询参数
 */
@Data
public class UserQo {
    /**
     * 用户id
     */
    private Long id;
     /**
     * 用户名
     */
    private String name;
}

/**
 * @description 用户导出VO
 */
@Data
public class UserVo {
    @ExcelProperty("用户id")
    private Long id;
    @ExcelProperty("用户名")
    private String username;
    @ExcelProperty("手机号")
    private String phone;
}

/**
 * @description 用户导出业务类
 */
@Component
public class UserExportService extends AbstractExportTemplate, UserVo> {
    @Override
    protected Class<UserVo> getEntityClass() {
        return UserVo.class;
    }
    @Override
    protected List<UserVo> readData(UserQo params) {
        // 1. 模拟从数据库查询用户数据
        UserVo userVo = new UserVo();
        userVo.setId(params.getId());
        userVo.setUsername("zhangsan");
        userVo.setPhone("13800000000");
        return List.of(userVo);
    }
}

用户导出service添加了@Component注解,正常情况导出数据从数据库中查询而来,所以真实业务中需要将模拟数据改为从数据库查询即可。

我们写个测试类来试下:

@RestController
@RequestMapping("/template")
public class TemplateController {
    @Resource
    private UserExportService userExportService;
    @GetMapping("/user")
    public void export(UserQo params, HttpServletResponse response) {
        // 1. 导出用户数据
        userExportService.export(new BaseExportParam(params, "user-info", response));
    }
}

结果:可以正常导出excel。

如果有新的业务也需要导出,我们只需要实现导出模板即可,比如城市信息,代码如下:

城市信息

/**
 * @description 城市导出查询参数
 */
@Data
public class CityQo {
    /**
     * 城市id
     */
    private Long id;
}

/**
 * @description 城市信息导出VO
 */
@Data
public class CityVo {
    @ExcelProperty("城市id")
    private Long id;
    @ExcelProperty("城市名称")
    private String cityName;
}

/**
 * @description 城市导出业务类
 */
@Component
public class CityExportService extends AbstractExportTemplate {
    @Override
    protected Class getEntityClass() {
        return CityVo.class;
    }
    @Override
    protected List readData(CityQo params) {
        // 1. 模拟从数据库查询城市数据
        CityVo cityVo = new CityVo();
        cityVo.setId(params.getId());
        cityVo.setCityName("beijing");
        return List.of(cityVo);
    }
}

新增一个测试方法试试:

@RestController
@RequestMapping("/template")
public class TemplateController {
    @Resource
    private UserExportService userExportService;
    @Resource
    private CityExportService cityExportService;
    @GetMapping("/user")
    public void export(UserQo params, HttpServletResponse response) {
        // 1. 导出用户数据
        userExportService.export(new BaseExportParam(params, "user-info", response));
    }
    @GetMapping("/city")
    public void exportCity(CityQo params, HttpServletResponse response) {
        // 1. 导出城市数据
        cityExportService.export(new BaseExportParam(params, "city-info", response));
    }
}

导出功能完全OK

模式优势

  1. 代码复用:将公共代码放在抽象类中,避免代码重复
  2. 扩展性:通过子类扩展新的实现,符合开闭原则
  3. 便于维护:算法结构固定,修改只需要在特定位置进行
  4. 控制反转:父类控制整体流程,子类实现具体细节

适用场景

  1. 多个类有相同的方法,但具体实现不同
  2. 需要控制子类的扩展,只允许扩展特定步骤
  3. 重要复杂的算法,需要分解为多个步骤
  4. 框架设计,定义流程骨架,让用户实现具体步骤

注意事项

  1. 模板方法应该声明为final,防止子类重写算法骨架
  2. 合理使用钩子方法,提供灵活的扩展点
  3. 避免过多的抽象方法,会增加子类的实现负担

责任编辑:CQITer新闻报料:400-888-8888   本站原创,未经授权不得转载
继续阅读
热新闻
推荐
关于我们联系我们免责声明隐私政策 友情链接