目录
- 问题是什么,为什么使用模板方法
- 模板设计模式是什么
- 实际的应用场景
一、问题
// 咖啡制作
class CoffeeMaker {fun makeCoffee() {boilWater()println("Brewing coffee grounds")pourInCup()println("Adding sugar and milk")}private fun boilWater() = println("Boiling water")private fun pourInCup() = println("Pouring into cup")
}// 茶制作
class TeaMaker {fun makeTea() {boilWater()println("Steeping tea bag")pourInCup()println("Adding lemon")}private fun boilWater() = println("Boiling water")private fun pourInCup() = println("Pouring into cup")
}
问题分析:
❌ 重复代码:boilWater() 和 pourInCup() 重复
❌ 维护困难:修改流程需要改动所有实现类
这里,我们就需要引出我们今天的主角,模板设计模式。
二、模板设计模式是什么
在父类中定义流程步骤,允许子类在不改变原有步骤的情况下重写特定步骤,下面直接上代码。
// 抽象模板类
abstract class BeverageMaker {// 模板方法(final禁止子类覆盖)final fun prepareBeverage() {boilWater()brew()pourInCup()addCondiments()}// 具体步骤实现private fun boilWater() = println("Boiling water")private fun pourInCup() = println("Pouring into cup")// 抽象方法(必须由子类实现)abstract fun brew()abstract fun addCondiments()
}// 具体实现类
class CoffeeMaker : BeverageMaker() {override fun brew() = println("Brewing coffee grounds")override fun addCondiments() = println("Adding sugar and milk")
}class TeaMaker : BeverageMaker() {override fun brew() = println("Steeping tea bag")override fun addCondiments() = println("Adding lemon")
}// 使用示例
fun main() {val coffee = CoffeeMaker()coffee.prepareBeverage()println("\n---------------\n")val tea = TeaMaker()tea.prepareBeverage()
}
可以看到,我们只需要使用抽象父类,子类进行具体实现,就可以完成模板设计模式。
带来的好处就是,重复代码没了。
2.1 接下来我们看一个实际的应用场景
我们有一个图片文件上传功能和图片url上传功能,这个功能里面,大部分逻辑是一样的,只有特定的不一样,我们看看。
图片文件上传功能
public UploadPictureResult uploadPicture(MultipartFile multipartFile, String uploadPathPrefix) {//校验图片:大小validPicture(multipartFile);//图片上传地址//1. 业务上传图片区分 2. 随机数 3.原始文件名称。使用原始文件名称会存在安全性问题//2. 文件前缀String uuid = RandomUtil.randomNumbers(16);String originalFilename = multipartFile.getOriginalFilename();String uploadFilename = String.format("%s_%s_%s", DateUtil.formatDate(new Date()), uuid,originalFilename);String uploadPath = String.format("/%s/%s",uploadPathPrefix,uploadFilename);//然后发送给对象存储File tempFile =null;try {//解析结果并返回tempFile = File.createTempFile(uploadPath, null);multipartFile.transferTo(tempFile);PutObjectResult putObjectResult = cosManager.putPictureObject(uploadPath, tempFile);ImageInfo imageInfo = putObjectResult.getCiUploadResult().getOriginalInfo().getImageInfo();int width = imageInfo.getWidth();int height = imageInfo.getHeight();double picScale = NumberUtil.round(width *1.0 /height,2).doubleValue();UploadPictureResult uploadPictureResult = new UploadPictureResult();uploadPictureResult.setUrl(cosClientConfig.getHost()+"/"+uploadPath);uploadPictureResult.setPicName(FileUtil.mainName(originalFilename));uploadPictureResult.setPicSize(FileUtil.size(tempFile));uploadPictureResult.setPicWidth(width);uploadPictureResult.setPicHeight(height);uploadPictureResult.setPicScale(picScale);uploadPictureResult.setPicFormat(imageInfo.getFormat());return uploadPictureResult;} catch (Exception e) {log.error("file upload error,filepath = "+ uploadPath,e);throw new BusinessException(ErrorCode.SYSTEM_ERROR,"上传失败");}finally {//临时文件清理deleteTempFile(tempFile);}}
图片url上传功能
public UploadPictureResult uploadPictureByUrl(String fileUrl, String uploadPathPrefix) {//校验图片:大小//validPicture(multipartFile);//todovalidPicture(fileUrl);//图片上传地址//1. 业务上传图片区分 2. 随机数 3.原始文件名称。使用原始文件名称会存在安全性问题//2. 文件前缀String uuid = RandomUtil.randomNumbers(16);//todo 获取图片原始名字//String originalFilename = multipartFile.getOriginalFilename();String originalFilename = FileUtil.mainName(fileUrl);String uploadFilename = String.format("%s_%s_%s", DateUtil.formatDate(new Date()), uuid,originalFilename);String uploadPath = String.format("/%s/%s",uploadPathPrefix,uploadFilename);//然后发送给对象存储File tempFile =null;try {//解析结果并返回tempFile = File.createTempFile(uploadPath, null);//todo 下载文件//multipartFile.transferTo(tempFile);HttpUtil.downloadFile(fileUrl,tempFile);PutObjectResult putObjectResult = cosManager.putPictureObject(uploadPath, tempFile);ImageInfo imageInfo = putObjectResult.getCiUploadResult().getOriginalInfo().getImageInfo();int width = imageInfo.getWidth();int height = imageInfo.getHeight();double picScale = NumberUtil.round(width *1.0 /height,2).doubleValue();UploadPictureResult uploadPictureResult = new UploadPictureResult();uploadPictureResult.setUrl(cosClientConfig.getHost()+"/"+uploadPath);uploadPictureResult.setPicName(FileUtil.mainName(originalFilename));uploadPictureResult.setPicSize(FileUtil.size(tempFile));uploadPictureResult.setPicWidth(width);uploadPictureResult.setPicHeight(height);uploadPictureResult.setPicScale(picScale);uploadPictureResult.setPicFormat(imageInfo.getFormat());return uploadPictureResult;} catch (Exception e) {log.error("file upload error,filepath = "+ uploadPath,e);throw new BusinessException(ErrorCode.SYSTEM_ERROR,"上传失败");}finally {//临时文件清理deleteTempFile(tempFile);}
}
这两份代码里面,只有三个地方不一样:
- validPicture(multipartFile);
- String originalFilename = multipartFile.getOriginalFilename();
- multipartFile.transferTo(tempFile);
其他的都是一样,我们可以使用模板方法进行抽象,但是这里有个问题就是,有个参数是String,有个参数是MultipartFile,类型不一样,怎么办呢?我们可以使用泛型或者Object来解决,这里我们就使用Object。
2.1.1 抽象一个父类
这个父类里面,有一个公共方法uploadPicture,参数为object,有三个抽象方法:validPicture、getOriginFilename以及processFile。
@Slf4j
public abstract class PictureUploadTemplate { @Resourceprotected CosManager cosManager;@Resource protected CosClientConfig cosClientConfig;/** * 模板方法,定义上传流程 */ public final UploadPictureResult uploadPicture(Object inputSource, String uploadPathPrefix) {// 1. 校验图片 validPicture(inputSource); // 2. 图片上传地址 String uuid = RandomUtil.randomString(16);String originFilename = getOriginFilename(inputSource); String uploadFilename = String.format("%s_%s.%s", DateUtil.formatDate(new Date()), uuid,FileUtil.getSuffix(originFilename));String uploadPath = String.format("/%s/%s", uploadPathPrefix, uploadFilename); File file = null; try { // 3. 创建临时文件 file = File.createTempFile(uploadPath, null);// 处理文件来源(本地或 URL) processFile(inputSource, file); // 4. 上传图片到对象存储 PutObjectResult putObjectResult = cosManager.putPictureObject(uploadPath, file);ImageInfo imageInfo = putObjectResult.getCiUploadResult().getOriginalInfo().getImageInfo();// 5. 封装返回结果 return buildResult(originFilename, file, uploadPath, imageInfo); } catch (Exception e) { log.error("图片上传到对象存储失败", e); throw new BusinessException(ErrorCode.SYSTEM_ERROR, "上传失败");} finally { // 6. 清理临时文件 deleteTempFile(file); } } /** * 校验输入源(本地文件或 URL) */ protected abstract void validPicture(Object inputSource); /** * 获取输入源的原始文件名 */ protected abstract String getOriginFilename(Object inputSource); /** * 处理输入源并生成本地临时文件 */ protected abstract void processFile(Object inputSource, File file) throws Exception; /** * 封装返回结果 */ private UploadPictureResult buildResult(String originFilename, File file, String uploadPath, ImageInfo imageInfo) { UploadPictureResult uploadPictureResult = new UploadPictureResult(); int picWidth = imageInfo.getWidth(); int picHeight = imageInfo.getHeight(); double picScale = NumberUtil.round(picWidth * 1.0 / picHeight, 2).doubleValue();uploadPictureResult.setPicName(FileUtil.mainName(originFilename)); uploadPictureResult.setPicWidth(picWidth); uploadPictureResult.setPicHeight(picHeight); uploadPictureResult.setPicScale(picScale); uploadPictureResult.setPicFormat(imageInfo.getFormat()); uploadPictureResult.setPicSize(FileUtil.size(file)); uploadPictureResult.setUrl(cosClientConfig.getHost() + "/" + uploadPath); return uploadPictureResult; } /** * 删除临时文件 */ public void deleteTempFile(File file) { if (file == null) { return; } boolean deleteResult = file.delete(); if (!deleteResult) { log.error("file delete error, filepath = {}", file.getAbsolutePath()); } }
}
2.1.2 子类实现
@Service
public class UrlPictureUpload extends PictureUploadTemplate { @Override protected void validPicture(Object inputSource) { String fileUrl = (String) inputSource; ThrowUtils.throwIf(StrUtil.isBlank(fileUrl), ErrorCode.PARAMS_ERROR, "文件地址不能为空");// ... 跟之前的校验逻辑保持一致 } @Override protected String getOriginFilename(Object inputSource) { String fileUrl = (String) inputSource; // 从 URL 中提取文件名 return FileUtil.mainName(fileUrl);} @Override protected void processFile(Object inputSource, File file) throws Exception {String fileUrl = (String) inputSource; // 下载文件到临时目录 HttpUtil.downloadFile(fileUrl, file);}
}
@Service
public class FilePictureUpload extends PictureUploadTemplate { @Override protected void validPicture(Object inputSource) { MultipartFile multipartFile = (MultipartFile) inputSource;ThrowUtils.throwIf(multipartFile == null, ErrorCode.PARAMS_ERROR, "文件不能为空");// 1. 校验文件大小 long fileSize = multipartFile.getSize(); final long ONE_M = 1024 * 1024L; ThrowUtils.throwIf(fileSize > 2 * ONE_M, ErrorCode.PARAMS_ERROR, "文件大小不能超过 2M"); // 2. 校验文件后缀 String fileSuffix = FileUtil.getSuffix(multipartFile.getOriginalFilename());// 允许上传的文件后缀 final List<String> ALLOW_FORMAT_LIST = Arrays.asList("jpeg", "jpg", "png", "webp");ThrowUtils.throwIf(!ALLOW_FORMAT_LIST.contains(fileSuffix), ErrorCode.PARAMS_ERROR, "文件类型错误"); } @Override protected String getOriginFilename(Object inputSource) { MultipartFile multipartFile = (MultipartFile) inputSource; return multipartFile.getOriginalFilename(); } @Override protected void processFile(Object inputSource, File file) throws Exception {MultipartFile multipartFile = (MultipartFile) inputSource; multipartFile.transferTo(file); }
}
2.1.3 进行使用
@Resource
private FilePictureUpload filePictureUpload;@Resource
private UrlPictureUpload urlPictureUpload;@Override
public PictureVO uploadPicture(Object inputSource, PictureUploadRequest pictureUploadRequest, User loginUser) {PictureUploadTemplate pictureUploadTemplate = filePictureUpload;if (inputSource instanceof String){pictureUploadTemplate = urlPictureUpload;}UploadPictureResult uploadPictureResult = pictureUploadTemplate.uploadPicture(inputSource, uploadPathPrefix);}
通过判断inputSource不同的类型来使用不同的模板子类。