今天我们来学习下模板模式,以及在项目中的应用
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😀😀😀😀
模式优势
- 代码复用:将公共代码放在抽象类中,避免代码重复
- 扩展性好:通过子类扩展新的实现,符合开闭原则
- 便于维护:算法结构固定,修改只需要在特定位置进行
- 控制反转:父类控制整体流程,子类实现具体细节
适用场景
- 多个类有相同的方法,但具体实现不同
- 需要控制子类的扩展,只允许扩展特定步骤
- 重要复杂的算法,需要分解为多个步骤
- 框架设计,定义流程骨架,让用户实现具体步骤
注意事项
- 模板方法应该声明为final,防止子类重写算法骨架
- 合理使用钩子方法,提供灵活的扩展点
- 避免过多的抽象方法,会增加子类的实现负担