目录
- 问题是什么,为什么使用模板方法
- 模板设计模式是什么
- 实际的应用场景
一、问题
kt
// 咖啡制作
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() 重复
❌ 维护困难:修改流程需要改动所有实现类
这里,我们就需要引出我们今天的主角,模板设计模式。
二、模板设计模式是什么
在父类中定义流程步骤,允许子类在不改变原有步骤的情况下重写特定步骤,下面直接上代码。
kt
// 抽象模板类
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上传功能,这个功能里面,大部分逻辑是一样的,只有特定的不一样,我们看看。
图片文件上传功能
java
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上传功能
java
public UploadPictureResult uploadPictureByUrl(String fileUrl, String uploadPathPrefix) {
//校验图片:大小
//validPicture(multipartFile);
//todo
validPicture(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。
java
@Slf4j
public abstract class PictureUploadTemplate {
@Resource
protected 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 子类实现
java
@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);
}
}
java
@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 进行使用
java
@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不同的类型来使用不同的模板子类。