《拒绝重复代码!模板模式教你优雅复用算法骨架》

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

01 模板模式简介

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

主要角色

  • 抽象类(Abstract Class) :定义算法骨架和抽象操作
  • 具体类(Concrete Class) :实现抽象类中的抽象操作

02 基础代码实现

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

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

java 复制代码
/**
 * @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() 方法就是泡茶方法,烧开水、添加开水是必需的步骤,唯有放入茶叶是可变的,下面我们分别实现使用不同茶叶泡茶的业务。

java 复制代码
/**
 * @description 绿茶
 */
public class GreenTea extends AbstractMakeTeaTemplate {
    @Override
    protected void putTea() {
        System.out.println("放入龙井茶叶");
    }
}
java 复制代码
/**
 * @description 红茶
 */
public class RedTea extends AbstractMakeTeaTemplate {
    @Override
    protected void putTea() {
        System.out.println("放入普尔茶叶");
    }
}
java 复制代码
/**
 * @description 玫瑰茶
 */
public class RoseTea extends AbstractMakeTeaTemplate {
    @Override
    protected void putTea() {
        System.out.println("放入玫瑰花茶叶");
    }
}

使用main方法测试下:

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

输出结果如下:

复制代码
烧开水
放入绿茶
加入水

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

03 项目实战

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

抽象模板:

java 复制代码
/**
 * @description 数据导出模板
 */
public abstract class AbstractExportTemplate<P, T> {

    /**
     * 导出数据
     * <p>声明为final类型,防止子类重写,确保导出流程的一致性</p>
     */
    public final void export(BaseExportParam<P> baseExportParam) {
        // 1. 校验参数
        validateParams(baseExportParam.getParams());
        // 2. 读取数据
        List<T> data = readData(baseExportParam.getParams());
        // 3. 导出数据
        doExport(baseExportParam, data);
        // 4. 导出完成后处理
        afterExport();
    }

    /**
     * 获取导出实体的Class对象
     * <p>子类必须实现此方法提供具体的实体类型</p>
     */
    protected abstract Class<T> getEntityClass();

    /**
     * 读取数据
     * <p>声明为abstract类型,强制要求子类实现,确保数据读取的一致性</p>
     */
    protected abstract List<T> readData(P params);

    /**
     * 校验参数
     * <p>子类根据自身业务情况,看是否需要进行参数校验</p>
     */
    protected void validateParams(P params) {
    }

    /**
     * 导出数据
     */
    private void doExport(BaseExportParam<P> baseExportParam, List<T> 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() {
        // 导出完成后处理逻辑(可空)
    }
}

模板入参基类:

java 复制代码
/**
 * @description 导出基类
 */
@Getter
public class BaseExportParam<P> {

    /**
     * 导出参数
     */
    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)模板类已经做了默认实现,子类视情况选择是否实现。

用户导出

java 复制代码
/**
 * @description 用户导出查询参数
 */
@Data
public class UserQo {
    /**
     * 用户id
     */
    private Long id;
     /**
     * 用户名
     */
    private String name;
}
java 复制代码
/**
 * @description 用户导出VO
 */
@Data
public class UserVo {
    @ExcelProperty("用户id")
    private Long id;
    @ExcelProperty("用户名")
    private String username;
    @ExcelProperty("手机号")
    private String phone;
}
scala 复制代码
/**
 * @description 用户导出业务类
 */
@Component
public class UserExportService extends AbstractExportTemplate<UserQo, 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注解,正常情况导出数据从数据库中查询而来,所以真实业务中需要将模拟数据改为从数据库查询即可。

我们写个测试类来试下:

less 复制代码
@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。

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

城市信息

java 复制代码
/**
 * @description 城市导出查询参数
 */
@Data
public class CityQo {
    /**
     * 城市id
     */
    private Long id;
}
java 复制代码
/**
 * @description 城市信息导出VO
 */
@Data
public class CityVo {
    @ExcelProperty("城市id")
    private Long id;
    @ExcelProperty("城市名称")
    private String cityName;
}
java 复制代码
/**
 * @description 城市导出业务类
 */
@Component
public class CityExportService extends AbstractExportTemplate<CityQo, CityVo> {
    @Override
    protected Class<CityVo> getEntityClass() {
        return CityVo.class;
    }

    @Override
    protected List<CityVo> readData(CityQo params) {
        // 1. 模拟从数据库查询城市数据
        CityVo cityVo = new CityVo();
        cityVo.setId(params.getId());
        cityVo.setCityName("beijing");
        return List.of(cityVo);
    }
}

新增一个测试方法试试:

java 复制代码
@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. 避免过多的抽象方法,会增加子类的实现负担
相关推荐
L.EscaRC2 小时前
ArkTS分布式设计模式浅析
分布式·设计模式·arkts
一起养条鱼吧2 小时前
🧩 Argon2 密码哈希
人工智能·后端
QZQ541882 小时前
使用C++实现一个简易的线程池
后端
shark_chili2 小时前
基于魔改Nightingale源码浅谈go语言包模块管理
后端
回家路上绕了弯2 小时前
用户中心微服务设计指南:从功能到非功能的全维度落地
后端·微服务
Main121382 小时前
Java Duration 完全指南:高精度时间间隔处理的利器
后端
用户3459474113612 小时前
Android系统中HAL层开发实例
后端
undefined在掘金390412 小时前
第二节 Node.js 项目实践 - 使用 nvm 安装 Node.js
后端
小码编匠2 小时前
.NET 10 性能突破:持续优化才是质变关键
后端·c#·.net