策略模式的实际应用:从单一数据源到多数据源架构

策略模式的实际应用:从单一数据源到多数据源架构


引言

策略模式(Strategy Pattern)是一种行为型设计模式,它定义了一系列算法,将每个算法封装起来,并使它们可以互换。本文通过一个真实的生产案例,展示如何在 Spring Boot 项目中应用策略模式,实现多数据源的灵活架构。


问题背景

业务场景

在某个业务功能中,需要从多个不同的数据源获取数据:

  1. 实时数据源:存储实时采集数据(如温度、压力、流量等)
  2. 检测数据源:存储化验检测数据(如成分分析、质量指标等)
  3. 未来扩展:可能还有其他数据源(如 ERP 系统、MES 系统等)

传统方案的问题

如果使用传统的 if-else 方式:

java 复制代码
// ❌ 不推荐:硬编码的数据源判断
public List<ReportData> fetchData(List<Parameter> parameters, LocalDate dataDate) {
    List<ReportData> result = new ArrayList<>();

    for (Parameter param : parameters) {
        if ("realtimeDataSource".equals(param.getDataSource())) {
            // 从实时数据源获取数据
            result.add(fetchFromRealtimeDataSource(param, dataDate));
        } else if ("labDataSource".equals(param.getDataSource())) {
            // 从检测数据源获取数据
            result.add(fetchFromLabDataSource(param, dataDate));
        } else if ("erpDataSource".equals(param.getDataSource())) {
            // 从 ERP 系统获取数据
            result.add(fetchFromErpDataSource(param, dataDate));
        }
        // 每次新增数据源都要修改这里的代码
    }

    return result;
}

问题

  • ❌ 违反开闭原则(对扩展开放,对修改关闭)
  • ❌ 每次新增数据源都要修改主业务逻辑
  • ❌ 代码臃肿,难以维护
  • ❌ 不同数据源的获取逻辑耦合在一起

策略模式设计

设计思路

核心思想

  1. 定义统一的策略接口
  2. 每个数据源实现独立的策略类
  3. 通过 Spring 容器动态获取对应的策略
  4. 主业务逻辑不关心具体实现

架构图

复制代码
┌─────────────────────────────────────────────────────────┐
│              ReportServiceImpl                          │
│                  (主业务逻辑)                            │
│  ┌───────────────────────────────────────────────────┐ │
│  │         ApplicationContext.getBean()              │ │
│  │        根据 data_source 动态获取策略               │ │
│  └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
        ┌─────────────────────────────────────┐
        │         DataStrategy                │
        │        (策略接口)                    │
        │  batchFetchData(parameters, date)   │
        └─────────────────────────────────────┘
                          │
          ┌───────────────┼───────────────┐
          ▼               ▼               ▼
  ┌──────────────────┐  ┌──────────────┐  ┌──────────────┐
  │ RealtimeDataSource│  │ LabDataSource│  │ ErpDataSource│
  │     Strategy     │  │   Strategy   │  │   Strategy   │
  │  (实时数据源)     │  │ (检测数据源)  │  │ (ERP系统)    │
  └──────────────────┘  └──────────────┘  └──────────────┘

策略接口定义

java 复制代码
package com.example.biz.strategy;

import com.example.biz.model.entity.ReportData;
import com.example.biz.model.entity.Parameter;
import java.time.LocalDate;
import java.util.List;

/**
 * 数据获取策略接口
 */
public interface DataStrategy {

    /**
     * 批量获取参数配置对应的数据
     *
     * @param parameters 参数配置列表
     * @param dataDate   数据日期
     * @return 数据列表
     */
    List<ReportData> batchFetchData(
            List<Parameter> parameters,
            LocalDate dataDate);
}

设计要点

  • ✅ 接口简洁,只有一个方法
  • ✅ 参数明确:参数配置列表 + 数据日期
  • ✅ 返回值清晰:统一的数据实体列表
  • ✅ 批量处理:一次调用处理多个参数,提升性能

策略实现

1. 实时数据源策略

java 复制代码
package com.example.biz.strategy.impl;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

import org.springframework.stereotype.Component;

import com.example.biz.model.entity.ReportData;
import com.example.biz.model.entity.Parameter;
import com.example.biz.strategy.DataStrategy;
import com.example.biz.support.dto.RealtimeValueDto;
import com.example.biz.support.service.IRealtimeDataService;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * 实时数据源策略
 * 从实时数据源获取采集数据
 */
@Slf4j
@Component("realtimeDataSource")
@RequiredArgsConstructor
public class RealtimeDataSourceStrategy implements DataStrategy {

    private final IRealtimeDataService realtimeDataService;

    @Override
    public List<ReportData> batchFetchData(
            List<Parameter> parameters,
            LocalDate dataDate) {

        // 转换日期范围
        Date startTime = Date.from(dataDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
        Date endTime = Date.from(dataDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
        Date date = Date.from(dataDate.atStartOfDay(ZoneId.systemDefault()).toInstant());

        // 提取所有 dataKey
        List<String> dataKeys = parameters.stream()
                .map(Parameter::getDataKey)
                .collect(Collectors.toList());

        // 批量查询数据
        Map<String, BigDecimal> dataMap = realtimeDataService.batchListByKey(
                dataKeys,
                startTime,
                endTime,
                RealtimeValueDto::toNameMaps
        ).orElse(Collections.emptyMap());

        // 构造返回数据列表
        List<ReportData> result = parameters.stream()
                .map(parameter -> {
                    BigDecimal value = dataMap.get(parameter.getDataKey());
                    if (value != null) {
                        ReportData data = new ReportData();
                        data.setParameterId(parameter.getId());
                        data.setDataDate(date);
                        data.setDataValue(value.doubleValue());
                        return data;
                    } else {
                        log.warn("未查询到数据: dataKey={}, date={}",
                                parameter.getDataKey(), dataDate);
                        return null;
                    }
                })
                .filter(Objects::nonNull)
                .collect(Collectors.toList());

        log.debug("批量查询数据完成: date={}, total={}, success={}",
                dataDate, parameters.size(), result.size());
        return result;
    }
}

关键点

  • @Component("realtimeDataSource"):指定 Bean 名称,与数据库中的 data_source 字段对应
  • 批量查询:一次性查询所有数据,提升性能
  • Stream API:使用 map 和 filter 进行数据转换
  • 容错处理:对查询不到的数据记录日志并过滤

2. 检测数据源策略

java 复制代码
package com.example.biz.strategy.impl;

import java.time.LocalDate;
import java.util.Collections;
import java.util.List;

import org.springframework.stereotype.Component;

import com.example.biz.model.entity.ReportData;
import com.example.biz.model.entity.Parameter;
import com.example.biz.strategy.DataStrategy;

import lombok.extern.slf4j.Slf4j;

/**
 * 检测数据源策略
 * 从检测系统获取化验数据
 */
@Slf4j
@Component("labDataSource")
public class LabDataSourceStrategy implements DataStrategy {

    @Override
    public List<ReportData> batchFetchData(
            List<Parameter> parameters,
            LocalDate dataDate) {
        // TODO: 实现检测数据批量查询逻辑
        log.warn("检测数据源策略暂未实现: date={}, count={}",
                dataDate, parameters.size());
        return Collections.emptyList();
    }
}

关键点

  • 同样实现 DataStrategy 接口
  • 可以有自己的业务逻辑和数据源
  • 返回空列表而不是 null,避免 NPE

主业务逻辑

动态获取策略

java 复制代码
@Slf4j
@Service
@RequiredArgsConstructor
public class ReportServiceImpl implements ReportService {

    private final ParameterMapper parameterMapper;
    private final ApplicationContext applicationContext;
    private final DataSaveService dataSaveService;

    @Override
    public void generateReportData(LocalDate localDate) {
        log.info("开始生成数据, 日期: {}", localDate);

        // 1. 查询所有生效的配置
        LambdaQueryWrapper<Parameter> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.isNotNull(Parameter::getDataSource);
        List<Parameter> parameters = parameterMapper.selectList(queryWrapper);

        // 2. 根据 data_source 分组
        Map<String, List<Parameter>> groupedByDataSource = parameters.stream()
                .collect(Collectors.groupingBy(Parameter::getDataSource));

        // 3. 批量获取所有数据源的数据
        List<ReportData> allDataList = groupedByDataSource.entrySet().stream()
                .flatMap(entry -> {
                    try {
                        String dataSource = entry.getKey();
                        List<Parameter> params = entry.getValue();

                        // 🔑 关键:通过 Spring 容器动态获取对应的策略 bean
                        DataStrategy strategy = applicationContext.getBean(
                                dataSource, DataStrategy.class);

                        // 批量调用策略获取数据
                        List<ReportData> dataList = strategy.batchFetchData(params, localDate);

                        if (dataList != null && !dataList.isEmpty()) {
                            log.debug("数据获取成功: dataSource={}, count={}", dataSource, dataList.size());
                            return dataList.stream();
                        } else {
                            log.warn("批量数据获取为空: dataSource={}", dataSource);
                            return Stream.empty();
                        }
                    } catch (Exception e) {
                        log.error("处理数据源异常: dataSource={}", entry.getKey(), e);
                        return Stream.empty();
                    }
                })
                .collect(Collectors.toList());

        // 4. 保存数据
        if (!allDataList.isEmpty()) {
            dataSaveService.saveData(allDataList);
            log.info("数据批量保存完成: date={}, total={}", localDate, allDataList.size());
        }
    }
}

核心代码解析

java 复制代码
// 🔑 核心:通过 Spring 容器动态获取策略 Bean
DataStrategy strategy = applicationContext.getBean(
        dataSource,              // 数据源名称,如 "realtimeDataSource"
        DataStrategy.class);     // 策略接口类型

工作流程

  1. 查询配置表,获取所有参数配置
  2. data_source 字段分组
  3. 遍历每个数据源:
    • 通过 applicationContext.getBean() 获取对应的策略实现
    • 调用策略的 batchFetchData() 方法获取数据
    • 使用 flatMap 将多个数据源的数据合并到一个列表
  4. 批量保存所有数据

代码演进过程

第一版:单个数据获取

java 复制代码
// ❌ 第一版:一次获取一个参数的数据
public interface DataStrategy {
    ReportData fetchData(
            Parameter parameter,
            LocalDate dataDate);
}

问题

  • 需要循环调用,性能差
  • 每次调用都要建立数据库连接

第二版:批量获取但返回 Map

java 复制代码
// ⚠️ 第二版:批量获取但返回 Map
public interface DataStrategy {
    Map<Long, BigDecimal> batchFetchData(
            List<Parameter> parameters,
            LocalDate dataDate);

    ReportData fetchData(
            Parameter parameter,
            LocalDate dataDate);
}

问题

  • 返回 Map,不够灵活
  • 有两个方法,接口复杂
  • 策略实现需要构造完整的对象

第三版:简化的批量接口(最终方案)

java 复制代码
// ✅ 第三版:简化的批量接口
public interface DataStrategy {
    List<ReportData> batchFetchData(
            List<Parameter> parameters,
            LocalDate dataDate);
}

优势

  • ✅ 接口简洁,只有一个方法
  • ✅ 策略实现直接构造完整对象
  • ✅ 返回类型明确
  • ✅ 支持批量处理

扩展性展示

如何添加新的数据源?

假设需要从 ERP 系统获取数据,只需三步:

步骤1:创建新的策略实现

java 复制代码
package com.example.biz.strategy.impl;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

import org.springframework.stereotype.Component;

import com.example.biz.model.entity.ReportData;
import com.example.biz.model.entity.Parameter;
import com.example.biz.strategy.DataStrategy;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * ERP 数据源策略
 * 从 ERP 系统获取数据
 */
@Slf4j
@Component("erpDataSource")
@RequiredArgsConstructor
public class ErpDataSourceStrategy implements DataStrategy {

    private final ErpService erpService;

    @Override
    public List<ReportData> batchFetchData(
            List<Parameter> parameters,
            LocalDate dataDate) {

        // 实现从 ERP 获取数据的逻辑
        return parameters.stream()
                .map(param -> {
                    // 调用 ERP 服务
                    BigDecimal value = erpService.getValue(
                            param.getErpField(), dataDate);

                    // 构造返回对象
                    ReportData data = new ReportData();
                    data.setParameterId(param.getId());
                    data.setDataDate(Date.from(dataDate.atStartOfDay(ZoneId.systemDefault()).toInstant()));
                    data.setDataValue(value.doubleValue());
                    return data;
                })
                .collect(Collectors.toList());
    }
}

步骤2:在数据库中添加配置

sql 复制代码
INSERT INTO report_parameter (
    data_source,
    tag_name,
    parameter_no,
    -- 其他字段
) VALUES (
    'erpDataSource',  -- 对应策略的 Bean 名称
    'ERP_FIELD_NAME',
    10,
    -- 其他字段
);

步骤3:测试

bash 复制代码
curl "http://localhost:8080/report/generate?dataDate=2026-01-08"

完成! 不需要修改任何主业务逻辑代码。


核心技术点

1. Spring 容器动态获取 Bean

java 复制代码
// 通过 Bean 名称 + 类型获取
DataStrategy strategy = applicationContext.getBean(
        "realtimeDataSource",
        DataStrategy.class);

原理

  • Spring 容器在启动时会扫描所有 @Component 注解的类
  • 将类名首字母小写作为默认 Bean 名称
  • 也可以通过 @Component("customName") 指定 Bean 名称
  • applicationContext.getBean() 可以通过名称和类型获取 Bean

2. 分组与扁平化处理

java 复制代码
// 按 data_source 分组
Map<String, List<Parameter>> grouped = parameters.stream()
        .collect(Collectors.groupingBy(Parameter::getDataSource));

// 扁平化合并多个数据源的数据
List<ReportData> allData = grouped.entrySet().stream()
        .flatMap(entry -> {
            List<ReportData> dataList = strategy.batchFetchData(...);
            return dataList.stream();  // 将 List 转换为 Stream
        })
        .collect(Collectors.toList());  // 收集到一个 List

flatMap 的作用

  • Stream<List<T>> 转换为 Stream<T>
  • 将多个列表合并成一个列表
  • 是 map + flatten 的组合操作

3. 策略模式的 Spring 实现

传统策略模式

  • 需要手动创建策略对象
  • 需要维护策略映射关系
  • 策略切换需要修改代码

Spring 策略模式

  • Spring 自动管理策略 Bean
  • 通过 Bean 名称动态获取策略
  • 数据库配置控制策略选择
  • 完全解耦,符合开闭原则

最佳实践总结

1. 策略接口设计

✅ 推荐做法

  • 接口简洁,方法数量少(1-2个)
  • 方法参数明确
  • 返回类型统一
  • 支持批量处理

❌ 避免做法

  • 接口过于复杂
  • 方法职责不清晰
  • 返回类型不一致

2. 策略实现命名

命名规范

  • Bean 名称:xxxDataSource(与数据库字段对应)
  • 类名:XxxDataSourceStrategy(便于理解)
  • 包路径:strategy.impl(统一管理)
java 复制代码
@Component("realtimeDataSource")  // Bean 名称
public class RealtimeDataSourceStrategy  // 类名
        implements DataStrategy {
}

3. 异常处理

推荐做法

  • 在主业务逻辑中捕获异常
  • 避免某个数据源失败影响整体流程
  • 记录详细的错误日志
java 复制代码
.flatMap(entry -> {
    try {
        // 调用策略
        return strategy.batchFetchData(...).stream();
    } catch (Exception e) {
        log.error("处理数据源异常: dataSource={}", entry.getKey(), e);
        return Stream.empty();  // 返回空流,不影响其他数据源
    }
})

4. 性能优化

批量处理优于单个处理

  • ✅ 批量查询数据库
  • ✅ 批量调用外部服务
  • ✅ 批量构造对象

对比

java 复制代码
// ❌ 单个处理:循环 100 次
for (Parameter param : parameters) {
    Data data = strategy.fetchData(param);  // 100 次调用
}

// ✅ 批量处理:1 次调用
List<Data> dataList = strategy.batchFetchData(parameters);  // 1 次调用

策略模式 vs 其他方案

方案对比

方案 优点 缺点 适用场景
if-else 简单直接 违反开闭原则、代码臃肿 数据源固定且很少
策略模式 符合开闭原则、扩展性好 类数量增多 数据源多样、经常扩展
工厂模式 集中管理对象创建 需要维护工厂类 对象创建复杂
Spring策略 自动管理、完全解耦 依赖 Spring 容器 Spring 项目

选择建议

  • 小型项目:if-else 足够
  • 中型项目:传统策略模式
  • Spring 项目:Spring 策略模式(推荐)

总结

核心要点

  1. 策略接口设计:简洁、明确、支持批量
  2. Spring 动态获取:通过 Bean 名称动态获取策略
  3. 完全解耦:主业务逻辑不依赖具体策略实现
  4. 开闭原则:新增数据源无需修改主业务逻辑
  5. 性能优化:批量处理优于单个处理

实战价值

通过策略模式,我们实现了:

  • ✅ 多数据源的统一管理
  • ✅ 新数据源的快速接入
  • ✅ 代码的可维护性和可扩展性
  • ✅ 符合 SOLID 原则

参考资料

相关推荐
2301_7806698610 小时前
List(特有方法、遍历方式、ArrayList底层原理、LinkedList底层原理,二者区别)
java·数据结构·后端·list
零度@10 小时前
Java 消息中间件 - ActiveMQ 保姆级全解2026
java·activemq·java-activemq
weixin_3993806910 小时前
TongWeb异常宕机问题分析
java·tomcat
小鸡脚来咯10 小时前
设计模式面试介绍指南
java·开发语言·单例模式
小北方城市网10 小时前
GEO 全场景智能生态:自适应架构重构与极限算力协同落地
开发语言·人工智能·python·重构·架构·量子计算
怦怦蓝10 小时前
详解 IntelliJ IDEA 中编写邮件发送功能(从环境搭建到实战落地)
java·spring boot·intellij-idea
DENG86230410 小时前
二、使用idea运行Quarkus项目及调试
java·intellij-idea·quarkus
sww_102610 小时前
Spring AI Structured-Output源码分析
java·人工智能·spring