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

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

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 小时前
GoF设计模式——解释器模式
java·后端·spring·设计模式
掘金码甲哥4 小时前
这块终端神器, 必须吹爆!
后端
糖果店的幽灵5 小时前
【DeepAgents 从入门到精通】Context Management 上下文管理
java·人工智能·后端·spring·中间件·langgraph·deepagents
Csvn5 小时前
📊 SQL 入门 Day 8:集合操作 — 用 SQL 做数学里的"并交差"
后端·sql
大数据新鸟7 小时前
设计模式四大基本原则
设计模式
码事漫谈8 小时前
告别数据孤岛与AI“水土不服”:金仓多模融合时序库如何让数据真正服务于业务
后端
IT_陈寒8 小时前
Redis的持久化配置把我坑惨了:你以为数据安全了?
前端·人工智能·后端
星栈8 小时前
Node 接口该写同步还是异步?
后端·node.js
红烧大青虫8 小时前
HarmonyOS应用开发实战:小事记 - UIAbility 的冷启动/热启动/后台启动三种场景与 launchParam 解析
后端·华为·harmonyos·鸿蒙系统
码事漫谈9 小时前
AI Token 缓存:命中省 10 倍,不命中白扔钱
后端