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

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

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. 避免过多的抽象方法,会增加子类的实现负担
相关推荐
文艺理科生4 分钟前
Nginx 路径映射深度解析:从本地开发到生产交付的底层哲学
前端·后端·架构
千寻girling4 分钟前
主管:”人家 Node 框架都用 Nest.js 了 , 你怎么还在用 Express ?“
前端·后端·面试
南极企鹅6 分钟前
springBoot项目有几个端口
java·spring boot·后端
Luke君607978 分钟前
Spring Flux方法总结
后端
define952712 分钟前
高版本 MySQL 驱动的 DNS 陷阱
后端
忧郁的Mr.Li1 小时前
SpringBoot中实现多数据源配置
java·spring boot·后端
暮色妖娆丶2 小时前
SpringBoot 启动流程源码分析 ~ 它其实不复杂
spring boot·后端·spring
BD_Marathon2 小时前
设计模式——依赖倒转原则
java·开发语言·设计模式
BD_Marathon2 小时前
设计模式——里氏替换原则
java·设计模式·里氏替换原则
Coder_Boy_2 小时前
Deeplearning4j+ Spring Boot 电商用户复购预测案例中相关概念
java·人工智能·spring boot·后端·spring