用「模板方法 + 工厂 + 策略」优化导出功能:我终于懂了设计模式的本质 —— 为问题而生,而非预先堆砌

在 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
  • 模板方法管流程:确保所有导出场景遵循统一流程,共性逻辑集中维护;
  • 策略模式管差异:每个模板子类都是独立策略,可灵活替换且互不影响;
  • 工厂模式管分配:集中管理所有策略,调用方无需关心 “怎么选策略”,只需 “要什么策略”。

此时新增导出场景(如 “商品导出”),只需做两步:

  1. 新建GoodsExportTemplate,继承AbstractExportTemplate并实现ExportStrategy;
  1. 实现抽象方法和策略方法,无需修改任何现有代码。

完全符合开闭原则,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注解标注类型,实现差异化步骤)​

​

这个架构的核心价值在于:​

  1. 共性逻辑集中化:Excel 生成、OSS 上传、异常处理等通用逻辑只在模板类中写一次,后续优化只需改一处;​
  2. 差异逻辑隔离化:每个导出场景的 “查什么数据、用什么表头” 独立在策略类中,互不影响;​
  3. 扩展成本极低:新增导出场景时,只需新建策略类 + 加两个注解,无需修改任何现有代码;​
  4. 问题定位清晰:流程问题找模板类,权限 / 筛选问题找策略类,注册问题找工厂类,职责划分明确。​

五、写在最后:设计模式的 “正确打开方式”​

通过这次导出功能的优化,我彻底摆脱了 “为用模式而用模式” 的误区,也总结出设计模式的三个 “正确打开方式”:​

  1. 先解决问题,再选模式:不要一上来就想 “我该用什么模式”,而是先想 “我要解决什么问题”—— 比如 “流程重复” 对应模板方法,“策略太多不好管理” 对应工厂 + 策略;​
  2. 宁简勿繁,拒绝过度设计:单一场景用简单方法,3-5 个场景用模板方法,10 + 个场景再加工厂 + 策略,不要用 “未来可能需要” 的理由,提前堆砌复杂架构;​
  3. 模式组合服务于业务:模板方法管流程、策略管差异、工厂管分配、注解管简化,所有模式的组合都是为了让业务代码更简洁、可维护,而非炫技。​

设计模式不是 “银弹”,也不是 “面试背书的工具”,而是 “解决特定问题的经验总结”。只有让模式追着问题走,而非让问题迁就模式,才能真正发挥它的价值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员小胡12138

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值