在 Java 开发中,我们常听到 “设计模式是银弹” 的说法,但实际项目里,不少人会陷入 “为用模式而用模式” 的误区 —— 明明一个简单方法能解决的问题,硬套三层抽象;明明需求还没规模化,却提前堆砌五六个模式。
最近我负责的导出功能,从单一列表迭代到多场景需求,恰好经历了 “无模式→模板方法→工厂 + 策略” 的演进过程。这个过程让我深刻体会到:设计模式的价值,从来不是 “炫技式的预先设计”,而是 “随问题复杂度升级的精准应对”。今天就以导出功能为例,聊聊设计模式该如何 “按需落地”,而非盲目堆砌。
一、导出功能的三段式演进:模式永远追着问题走
导出功能的核心需求很简单:查询数据→生成 Excel→上传文件。但随着业务场景增多,问题的复杂度在不断变化,对应的解决方案也必须跟着升级 —— 这正是 “模式为问题而生” 的最佳实践。
1. 初始阶段:单一导出场景(无模式 = 最优解)
最开始只有 “用户列表导出” 一个需求,逻辑清晰且固定:
- 接收查询参数→调用 UserDAO 查数据→用 EasyExcel 生成文件→上传到 OSS。
这时我没有引入任何抽象类或接口,直接在 Service 里写了一个exportUser方法,代码不到 50 行。有人说 “你这不符合面向对象设计”,但实际情况是:
- 需求简单,额外的抽象会增加冗余(比如写一个空的ExportStrategy接口,再写一个UserExportStrategy实现类,完全没必要);
- 开发效率最高,改逻辑时直接改一个方法,不用跳多个类;
- 后续若需求不变,这个方案能一直用下去。
结论:当问题足够简单时,“无模式” 就是最好的模式。设计模式的第一原则是 “不增加不必要的复杂度”。
2. 中期阶段:3-5 个导出场景(模板方法模式 = 解决 “重复流程 + 差异化步骤”)
随着业务扩展,陆续加了 “订单列表”“商品列表”“会员列表” 导出。这时发现两个关键矛盾:
- 流程重复:每个导出方法都要写 “Excel 生成(创建 Writer、设置表头、写入数据)”“文件上传(OSS 连接、上传、获取 URL)” 的代码,这些逻辑完全一致;
- 步骤差异:只有两处个性化逻辑 ——“查什么数据”(调用不同 DAO)、“Excel 列怎么映射”(表头与字段的对应关系不同)。
这正是模板方法模式的典型应用场景:当多个业务有 “相同核心流程,不同个性化步骤” 时,用抽象类定义 “流程模板”,将差异化步骤抽象为方法,交由子类实现。于是我设计了AbstractExportTemplate模板类:
步骤 1:定义模板类(固定流程,暴露差异)
// 模板方法核心类:定义导出的“流程模板”
public abstract class AbstractExportTemplate {
// 注入通用工具(子类共享,无需重复注入)
@Autowired
protected ExcelGenerator excelGenerator;
@Autowired
protected OSSUploader ossUploader;
// 【模板方法】固定导出核心流程,子类不可重写(final修饰)
public final String executeExport(Map<String, Object> params) {
try {
// 步骤1:查询数据(差异化步骤→抽象方法)
List<?> dataList = queryData(params);
// 步骤2:构建Excel表头(差异化步骤→抽象方法)
List<String> headerNames = buildHeaderNames();
List<String> fieldNames = buildFieldNames();
// 步骤3:生成Excel文件(共性流程→模板类实现)
File excelFile = generateExcel(headerNames, fieldNames, dataList);
// 步骤4:上传OSS并返回URL(共性流程→模板类实现)
return uploadToOSS(excelFile);
} catch (Exception e) {
// 步骤5:统一异常处理(共性流程→模板类实现)
throw new BusinessException("导出失败:" + e.getMessage());
}
}
// ---------------------- 差异化步骤(抽象方法,子类必须实现) ----------------------
/**
* 个性化查询数据:不同导出场景调用不同DAO
*/
protected abstract List<?> queryData(Map<String, Object> params);
/**
* 个性化构建Excel表头名称(如["用户ID","用户名"])
*/
protected abstract List<String> buildHeaderNames();
/**
* 个性化构建数据字段名(与表头对应,如["id","userName"])
*/
protected abstract List<String> fieldNames();
// ---------------------- 共性流程(模板类实现,子类直接复用) ----------------------
/**
* 生成Excel文件:所有导出场景共用同一套逻辑
*/
private File generateExcel(List<String> headerNames, List<String> fieldNames, List<?> dataList) {
// 1. 创建Excel写入器
ExcelWriter writer = EasyExcel.write().head(buildExcelHead(headerNames)).build();
// 2. 写入数据(通过反射映射字段与表头)
WriteSheet sheet = EasyExcel.writerSheet("导出数据").build();
writer.write(dataList, sheet);
// 3. 关闭流并返回文件
writer.finish();
return new File("temp_export.xlsx");
}
/**
* 上传OSS:所有导出场景共用同一套上传逻辑
*/
private String uploadToOSS(File file) {
String ossKey = "export/" + System.currentTimeMillis() + ".xlsx";
OSSClient ossClient = new OSSClient(endpoint, accessKey, secretKey);
ossClient.putObject(bucketName, ossKey, file);
return "https://" + bucketName + "." + endpoint + "/" + ossKey;
}
/**
* 辅助方法:构建EasyExcel需要的表头格式(共性逻辑)
*/
private List<List<String>> buildExcelHead(List<String> headerNames) {
return headerNames.stream().map(Collections::singletonList).collect(Collectors.toList());
}
}
步骤 2:子类实现差异化步骤(聚焦业务,无需关注流程)
每个导出场景只需继承模板类,实现 3 个抽象方法,不用再写重复的 Excel 生成和上传逻辑。比如 “用户导出” 子类:
@Service
public class UserExportTemplate extends AbstractExportTemplate {
@Autowired
private UserDAO userDAO;
@Override
protected List<?> queryData(Map<String, Object> params) {
// 只关注“查用户数据”:根据参数调用UserDAO
String userName = (String) params.get("userName");
Integer status = (Integer) params.get("status");
return userDAO.selectByCondition(userName, status);
}
@Override
protected List<String> buildHeaderNames() {
// 只关注“用户Excel表头”
return Arrays.asList("用户ID", "用户名", "手机号", "注册时间");
}
@Override
protected List<String> fieldNames() {
// 只关注“字段与表头的映射”
return Arrays.asList("id", "userName", "phone", "registerTime");
}
}
“订单导出” 子类同理,代码极简且聚焦:
@Service
public class OrderExportTemplate extends AbstractExportTemplate {
@Autowired
private OrderDAO orderDAO;
@Override
protected List<?> queryData(Map<String, Object> params) {
String orderNo = (String) params.get("orderNo");
Date startTime = (Date) params.get("startTime");
return orderDAO.selectByTimeRange(orderNo, startTime);
}
@Override
protected List<String> buildHeaderNames() {
return Arrays.asList("订单号", "金额", "支付状态", "下单时间");
}
@Override
protected List<String> fieldNames() {
return Arrays.asList("orderNo", "amount", "payStatus", "createTime");
}
}
为什么选模板方法,而非直接用策略?
- 模板方法强绑定流程:通过final修饰模板方法,强制所有子类遵循 “查数据→生成 Excel→上传” 的流程,避免子类乱改流程导致的 bug;
- 子类零流程负担:子类只需关注 “做什么(查什么数据、用什么表头)”,不用关心 “怎么做(Excel 怎么生成、OSS 怎么传)”,开发效率提升 50%;
- 共性逻辑集中维护:若后续要优化 Excel 生成逻辑(如加密码保护),只需改模板类的generateExcel方法,所有子类自动复用,无需逐个修改。
3. 后期阶段:10 + 个导出场景(模板方法 + 工厂 + 策略 = 解决 “规模化管理”)
当导出场景增加到 10 + 个时,新的问题超出了模板方法的能力范围:
- 调用方逻辑臃肿:前端传入 “导出类型”(如user“order”“goods”),后端需要根据类型选择对应的模板子类,若用if-else判断,代码会非常冗余:
// 反例:臃肿的调用逻辑
if ("user".equals(exportType)) {
return userExportTemplate.executeExport(params);
} else if ("order".equals(exportType)) {
return orderExportTemplate.executeExport(params);
} else if ("goods".equals(exportType)) {
// ... 10+个分支,新增场景还要改这里
}
- 子类管理混乱:10 + 个模板子类分散在项目中,调用方需要注入多个子类,代码冗余且易出错;
- 违反开闭原则:新增导出场景时,必须修改调用方的if-else逻辑,不符合 “对扩展开放,对修改关闭” 的设计原则。
这时需要在模板方法的基础上,引入策略模式 + 工厂模式:用策略模式统一模板子类的行为,用工厂模式集中管理策略(模板子类),三者配合解决 “规模化” 问题。
步骤 1:定义策略接口(统一模板子类的行为)
模板子类本质是 “不同的导出策略”,所以先定义ExportStrategy接口,明确策略的标准行为:
// 导出策略接口:统一所有模板子类的行为
public interface ExportStrategy {
// 执行导出(与模板类的executeExport方法对齐)
String doExport(Map<String, Object> params);
// 获取导出类型(用于工厂匹配)
String getExportType();
}
步骤 2:模板子类实现策略接口(模板与策略结合)
让原有的模板子类实现ExportStrategy接口,既保留模板方法的流程优势,又具备策略的可替换性:
@Service
public class UserExportTemplate extends AbstractExportTemplate implements ExportStrategy {
// ... 原有queryData、buildHeaderNames、fieldNames方法不变 ...
// 实现策略接口:执行导出(直接调用模板类的executeExport)
@Override
public String doExport(Map<String, Object> params) {
return super.executeExport(params);
}
// 实现策略接口:标识导出类型
@Override
public String getExportType() {
return "user";
}
}
@Service
public class OrderExportTemplate extends AbstractExportTemplate implements ExportStrategy {
// ... 原有方法不变 ...
@Override
public String doExport(Map<String, Object> params) {
return super.executeExport(params);
}
@Override
public String getExportType() {
return "order";
}
}
步骤 3:策略工厂(集中管理所有模板子类)
用工厂类扫描所有实现ExportStrategy的模板子类,按 “导出类型” 存入映射,彻底消除if-else:
@Service
public class ExportStrategyFactory implements ApplicationContextAware {
// 存储“导出类型→策略(模板子类)”的映射
private Map<String, ExportStrategy> strategyMap = new HashMap<>();
// Spring启动时,自动扫描所有策略Bean(模板子类)
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
// 1. 扫描所有实现ExportStrategy的Bean
Map<String, ExportStrategy> beans = applicationContext.getBeansOfType(ExportStrategy.class);
// 2. 按导出类型存入map
beans.values().forEach(strategy -> {
String exportType = strategy.getExportType();
strategyMap.put(exportType, strategy);
});
}
// 对外提供“按类型获取策略”的方法
public ExportStrategy getStrategy(String exportType) {
ExportStrategy strategy = strategyMap.get(exportType);
if (strategy == null) {
throw new IllegalArgumentException("不支持的导出类型:" + exportType);
}
return strategy;
}
}
步骤 4:统一调用入口(上下文)
封装上下文类,对外提供简洁的调用接口,调用方无需感知工厂和策略:
@Service
public class ExportContext {
@Autowired
private ExportStrategyFactory strategyFactory;
// 统一导出入口:调用方只需传“类型”和“参数”
public String export(String exportType, Map<String, Object> params) {
// 1. 工厂获取对应的策略(模板子类)
ExportStrategy strategy = strategyFactory.getStrategy(exportType);
// 2. 执行导出(策略调用模板方法)
return strategy.doExport(params);
}
}
三者配合的优势:1+1+1>3
- 模板方法管流程:确保所有导出场景遵循统一流程,共性逻辑集中维护;
- 策略模式管差异:每个模板子类都是独立策略,可灵活替换且互不影响;
- 工厂模式管分配:集中管理所有策略,调用方无需关心 “怎么选策略”,只需 “要什么策略”。
此时新增导出场景(如 “商品导出”),只需做两步:
- 新建GoodsExportTemplate,继承AbstractExportTemplate并实现ExportStrategy;
- 实现抽象方法和策略方法,无需修改任何现有代码。
完全符合开闭原则,10 + 个导出场景的管理成本大幅降低。
二、从演进过程看设计模式的本质:三个核心认知
回顾导出功能 “无模式→模板方法→模板 + 工厂 + 策略” 的演进,我对 “设计模式” 有了更深刻的理解 —— 它不是 “孤立的工具”,而是 “根据问题复杂度组合使用的解决方案”。
1. 模式的引入逻辑:先解决 “流程重复”,再解决 “管理混乱”
- 中期 3-5 个场景:核心痛点是 “流程重复”→用模板方法模式固定流程,减少重复代码;
- 后期 10 + 个场景:核心痛点是 “策略管理”→在模板方法基础上,加策略 + 工厂解决规模化问题。
反例:若一开始就用 “模板 + 工厂 + 策略” 处理单一场景,会导致 “过度设计”——50 行能解决的问题,硬写成 8 个类(模板类、策略接口、工厂、上下文、子类),维护成本翻倍。
2. 模式的组合原则:“主模式” 解决核心问题,“辅助模式” 解决衍生问题
在导出功能中:
- 主模式:模板方法,解决 “重复流程 + 差异化步骤” 的核心痛点;
- 辅助模式:策略 + 工厂,解决 “主模式带来的子类管理” 衍生问题。
所有模式组合都应遵循 “主辅分明”—— 先确定核心问题,用主模式解决,再用辅助模式处理主模式带来的新问题,而非盲目堆砌多个模式。
3. 模式的落地关键:理解 “模式的适用边界”
每个模式都有明确的适用边界,超出边界则失效:
- 模板方法的边界:适合 “流程固定、步骤差异” 的场景,若流程不固定(如有的导出需加压缩,有的不需要),则需配合装饰器模式;
- 策略模式的边界:适合 “策略可替换、数量较多” 的场景,若策略只有 2-3 个,用if-else反而更简单;
- 工厂模式的边界:适合 “策略创建复杂、需要集中管理” 的场景,若策略创建简单(直接new),则无需工厂。
三、进一步优化:用 “注解 + 扫描” 简化策略注册
当前方案中,每个策略类都要实现getExportType()方法,返回固定的类型字符串(如return "user"),10 + 个策略会有重复的 “字符串硬编码”,且容易出现 “类型标识与方法返回值不一致” 的问题。
优化方案:自定义注解标记导出类型,项目启动时通过注解扫描自动注册策略,彻底消除硬编码。
1. 自定义策略注解
// 标记策略类对应的导出类型
@Target(ElementType.TYPE) // 作用于类
@Retention(RetentionPolicy.RUNTIME) // 运行时保留,用于反射扫描
public @interface ExportType {
String value(); // 导出类型标识(如"user"、"order")
}
2. 策略类用注解标记(移除 getExportType 方法)
// 用户导出策略:用注解指定类型,无需实现getExportType
@Service
@ExportType("user")
public class UserExportStrategy extends AbstractExportStrategy {
@Autowired
private UserDAO userDAO;
@Override
public List<?> queryData(Map<String, Object> params) {
return userDAO.selectByParams(params);
}
@Override
public Map<String, String> buildHeader() {
Map<String, String> header = new HashMap<>();
header.put("用户ID", "id");
header.put("用户名", "userName");
return header;
}
}
// 订单导出策略:同理简化
@Service
@ExportType("order")
public class OrderExportStrategy extends AbstractExportStrategy {
@Autowired
private OrderDAO orderDAO;
@Override
public List<?> queryData(Map<String, Object> params) {
return orderDAO.selectByParams(params);
}
@Override
public Map<String, String> buildHeader() {
Map<String, String> header = new HashMap<>();
header.put("订单号", "orderNo");
header.put("金额", "amount");
return header;
}
}
3. 工厂扫描注解注册策略(修改逻辑)
@Service
public class ExportStrategyFactory implements ApplicationContextAware {
private Map<String, ExportStrategy> strategyMap = new HashMap<>();
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map<String, ExportStrategy> beans = applicationContext.getBeansOfType(ExportStrategy.class);
beans.values().forEach(strategy -> {
// 从注解中获取导出类型,替代原有的getExportType方法
ExportType annotation = strategy.getClass().getAnnotation(ExportType.class);
if (annotation != null) {
String exportType = annotation.value();
strategyMap.put(exportType, strategy);
}
});
}
public ExportStrategy getStrategy(String exportType) {
ExportStrategy strategy = strategyMap.get(exportType);
if (strategy == null) {
throw new IllegalArgumentException("不支持的导出类型:" + exportType);
}
return strategy;
}
}
优化后,策略类代码更简洁,新增策略时只需加@ExportType注解,无需手动实现类型返回方法,同时避免了硬编码带来的潜在错误。
四、最终架构总结:从 “代码冗余” 到 “可扩展架构” 的蜕变
回顾整个导出功能的演进,从 “单一接口硬编码” 到 “模板方法 + 工厂 + 策略 + 注解” 的组合方案,本质是“问题复杂度升级→解决方案升级”的过程,最终架构可总结为下图逻辑:
调用方(Controller) → 导出上下文(ExportContext) → 策略工厂(ExportStrategyFactory)
↓
抽象模板类(AbstractExportTemplate)← 具体策略类(User/Order/GoodsExportTemplate)
(固定流程:查数据→生成Excel→上传) (@ExportType注解标注类型,实现差异化步骤)
这个架构的核心价值在于:
- 共性逻辑集中化:Excel 生成、OSS 上传、异常处理等通用逻辑只在模板类中写一次,后续优化只需改一处;
- 差异逻辑隔离化:每个导出场景的 “查什么数据、用什么表头” 独立在策略类中,互不影响;
- 扩展成本极低:新增导出场景时,只需新建策略类 + 加两个注解,无需修改任何现有代码;
- 问题定位清晰:流程问题找模板类,权限 / 筛选问题找策略类,注册问题找工厂类,职责划分明确。
五、写在最后:设计模式的 “正确打开方式”
通过这次导出功能的优化,我彻底摆脱了 “为用模式而用模式” 的误区,也总结出设计模式的三个 “正确打开方式”:
- 先解决问题,再选模式:不要一上来就想 “我该用什么模式”,而是先想 “我要解决什么问题”—— 比如 “流程重复” 对应模板方法,“策略太多不好管理” 对应工厂 + 策略;
- 宁简勿繁,拒绝过度设计:单一场景用简单方法,3-5 个场景用模板方法,10 + 个场景再加工厂 + 策略,不要用 “未来可能需要” 的理由,提前堆砌复杂架构;
- 模式组合服务于业务:模板方法管流程、策略管差异、工厂管分配、注解管简化,所有模式的组合都是为了让业务代码更简洁、可维护,而非炫技。
设计模式不是 “银弹”,也不是 “面试背书的工具”,而是 “解决特定问题的经验总结”。只有让模式追着问题走,而非让问题迁就模式,才能真正发挥它的价值。

被折叠的 条评论
为什么被折叠?



