如何用策略模式解耦复杂数据处理链路?

59 阅读10分钟

在做日志检索平台时,我们遇到了一个典型的业务痛点:系统里跑着两个 ES 集群 —— 一个是普通集群用于实时数据查询,另一个是做了冷热分离的集群用于历史数据归档。这就导致 “ES 检索→数据转换→结果过滤” 的全流程里,每个步骤都要适配不同场景:检索时要连不同集群,转换时要按业务输出列表 / 统计两种格式,过滤时还要区分敏感词、权限两种规则。

最开始用if-else硬编码处理,代码里满是 “如果是冷热集群就走 A 逻辑,如果是普通集群就走 B 逻辑” 的判断,新增一个过滤规则就要改一遍主流程代码,维护起来像拆炸弹。后来引入策略模式 + 工厂模式的组合,不仅把代码解耦得清清楚楚,后续加新集群、新规则也只用加代码不用改代码。今天就结合这个实战场景,聊聊怎么用设计模式解决 “多场景差异化流程” 问题。

一、先明确问题:我们到底在解决什么?

在动手写代码前,得先把 “混乱的根源” 拎清楚。我们的核心矛盾是 “同一个流程步骤,存在多种差异化实现,且需要按业务条件动态切换”,具体体现在三个环节:

流程步骤差异化场景核心诉求
ES 检索1. 普通 ES 集群(实时数据)2. 冷热分离 ES 集群(历史数据)按 “数据类型” 动态选择连接的 ES 集群,查询逻辑要适配不同集群特性(如冷热集群需指定索引路由)
数据转换1. 输出 “列表格式”(前端表格展示)2. 输出 “统计格式”(前端图表展示)按 “前端需求” 动态选择转换逻辑,ES 原始结果要转成不同结构
结果过滤1. 敏感词过滤(屏蔽违规内容)2. 权限过滤(隐藏无权限数据)按 “业务规则” 动态选择过滤策略,部分场景需组合多种过滤规则

如果继续用if-else,新增一个 “地域过滤” 规则就要在过滤环节加一段判断,新增一个 “聚合检索” 逻辑就要在检索环节加分支 —— 代码会越来越臃肿,而且很容易改漏逻辑。这时候就需要策略模式出场,把 “可变的逻辑” 和 “固定的流程” 拆分开。

二、策略模式落地:三步搭建可扩展的流程框架

策略模式的核心思路很简单: “把每个步骤的差异化逻辑封装成独立‘策略’,流程只定义‘做什么’,具体‘怎么做’交给策略去实现” 。配合工厂模式管理策略的创建,就能实现 “动态选策略,流程不改动”。

第一步:定义策略接口,统一 “沟通语言”

不管每个步骤有多少种实现,先定义统一的接口,明确输入输出 —— 这一步是 “解耦的基础”,让流程只依赖接口,不依赖具体实现。

go

运行

// 1. ES检索策略接口:所有ES检索逻辑都要实现这个接口
type SearchStrategy interface {
    // 入参:ES查询条件(如DSL语句);出参:ES原始返回结果、错误信息
    Search(query map[string]interface{}) (interface{}, error)
}

// 2. 数据转换策略接口:所有转换逻辑都要实现这个接口
type ConvertStrategy interface {
    // 入参:ES原始结果;出参:转换后的目标格式数据、错误信息
    Convert(rawData interface{}) (interface{}, error)
}

// 3. 结果过滤策略接口:所有过滤逻辑都要实现这个接口
type FilterStrategy interface {
    // 入参:转换后的数据;出参:过滤后的最终数据、错误信息
    Filter(convertedData interface{}) (interface{}, error)
}

第二步:实现具体策略,封装差异化逻辑

针对每个步骤的不同场景,写对应的策略实现类 —— 这一步是 “把可变逻辑藏起来”,每个策略只关心自己的业务,互不干扰。

场景 1:ES 检索策略(适配两个集群)

go

运行

// 普通ES集群检索策略:处理实时数据查询
type NormalESSearcher struct {
    client *es.Client // 普通ES客户端(初始化时注入)
}

func (n *NormalESSearcher) Search(query map[string]interface{}) (interface{}, error) {
    // 普通集群查询逻辑:直接执行DSL,无需额外处理
    resp, err := n.client.Search().
        Index("realtime_logs").
        BodyJson(query).
        Do(context.Background())
    if err != nil {
        return nil, fmt.Errorf("normal es search failed: %v", err)
    }
    return resp, nil
}

// 冷热分离ES集群检索策略:处理历史数据查询(需指定冷热索引)
type HotColdESSearcher struct {
    client *es.Client // 冷热ES客户端(初始化时注入)
}

func (h *HotColdESSearcher) Search(query map[string]interface{}) (interface{}, error) {
    // 冷热集群特殊处理:根据查询时间范围路由到hot/cold索引
    index := getHotColdIndexByTime(query["start_time"].(int64)) // 自定义逻辑:按时间选索引
    resp, err := h.client.Search().
        Index(index).
        BodyJson(query).
        Do(context.Background())
    if err != nil {
        return nil, fmt.Errorf("hot-cold es search failed: %v", err)
    }
    return resp, nil
}

场景 2:数据转换策略(适配两种输出格式)

go

运行

// 列表格式转换策略:给前端表格用,只保留核心字段
type ListConvertor struct{}

func (l *ListConvertor) Convert(rawData interface{}) (interface{}, error) {
    esResp := rawData.(*esapi.SearchResponse)
    var list []map[string]interface{}
    for _, hit := range esResp.Hits.Hits {
        source := make(map[string]interface{})
        if err := json.Unmarshal(hit.Source_, &source); err != nil {
            return nil, err
        }
        // 只保留需要的字段:日志ID、内容、时间
        list = append(list, map[string]interface{}{
            "log_id": source["log_id"],
            "content": source["content"],
            "create_time": source["create_time"],
        })
    }
    return list, nil
}

// 统计格式转换策略:给前端图表用,输出聚合结果
type StatConvertor struct{}

func (s *StatConvertor) Convert(rawData interface{}) (interface{}, error) {
    esResp := rawData.(*esapi.SearchResponse)
    // 解析ES聚合结果(假设按“日志类型”聚合)
    agg, ok := esResp.Aggregations["log_type_agg"].(map[string]interface{})
    if !ok {
        return nil, fmt.Errorf("stat convert: invalid agg format")
    }
    buckets := agg["buckets"].([]interface{})
    var stat []map[string]interface{}
    for _, b := range buckets {
        bucket := b.(map[string]interface{})
        stat = append(stat, map[string]interface{}{
            "log_type": bucket["key"],
            "count": bucket["doc_count"],
        })
    }
    return stat, nil
}

场景 3:结果过滤策略(适配两种过滤规则)

go

运行

// 敏感词过滤策略:屏蔽内容中的违规词
type SensitiveFilter struct {
    sensitiveWords map[string]bool // 敏感词库(初始化时加载)
}

func (s *SensitiveFilter) Filter(convertedData interface{}) (interface{}, error) {
    list := convertedData.([]map[string]interface{})
    for i, item := range list {
        content := item["content"].(string)
        // 替换敏感词(这里简化为“***”)
        for word := range s.sensitiveWords {
            content = strings.ReplaceAll(content, word, "***")
        }
        list[i]["content"] = content
    }
    return list, nil
}

// 权限过滤策略:根据用户角色隐藏无权限数据
type PermissionFilter struct {
    currentUserRole string // 当前用户角色(从上下文获取)
}

func (p *PermissionFilter) Filter(convertedData interface{}) (interface{}, error) {
    list := convertedData.([]map[string]interface{})
    var filteredList []map[string]interface{}
    for _, item := range list {
        // 假设:管理员能看所有日志,普通用户只能看自己的
        if p.currentUserRole == "admin" || item["user_id"] == getCurrentUserID() {
            filteredList = append(filteredList, item)
        }
    }
    return filteredList, nil
}

第三步:用工厂模式管理策略,实现 “动态选择”

有了策略接口和实现,还需要一个 “策略工厂” 来根据业务条件创建对应的策略实例 —— 这一步是 “让流程不用关心怎么选策略”,把选择逻辑集中在工厂里。

go

运行

// 1. ES检索策略工厂:根据“数据类型”选策略
type SearchStrategyFactory struct {
    normalClient *es.Client   // 普通ES客户端
    hotColdClient *es.Client  // 冷热ES客户端
}

func (f *SearchStrategyFactory) Create(dataType string) SearchStrategy {
    switch dataType {
    case "realtime": // 实时数据→用普通ES策略
        return &NormalESSearcher{client: f.normalClient}
    case "history":  // 历史数据→用冷热ES策略
        return &HotColdESSearcher{client: f.hotColdClient}
    default:
        return &NormalESSearcher{client: f.normalClient} // 默认策略
    }
}

// 2. 转换策略工厂:根据“输出格式”选策略
type ConvertStrategyFactory struct{}

func (f *ConvertStrategyFactory) Create(outputType string) ConvertStrategy {
    switch outputType {
    case "list": // 表格→列表转换
        return &ListConvertor{}
    case "stat": // 图表→统计转换
        return &StatConvertor{}
    default:
        return &ListConvertor{}
    }
}

// 3. 过滤策略工厂:根据“过滤规则”选策略
type FilterStrategyFactory struct {
    sensitiveWords map[string]bool // 敏感词库
    currentUserRole string         // 当前用户角色
}

func (f *FilterStrategyFactory) Create(filterRule string) FilterStrategy {
    switch filterRule {
    case "sensitive": // 敏感词过滤
        return &SensitiveFilter{sensitiveWords: f.sensitiveWords}
    case "permission": // 权限过滤
        return &PermissionFilter{currentUserRole: f.currentUserRole}
    default:
        return &SensitiveFilter{sensitiveWords: f.sensitiveWords}
    }
}

三、组装流程:让代码 “按配置运行”

最后一步是把 “策略选择” 和 “流程执行” 结合起来,写一个统一的处理器 —— 这里的流程是固定的(检索→转换→过滤),但每个步骤用什么策略,全靠外部参数控制。

go

运行

// 数据处理核心处理器
type DataProcessor struct {
    searchFactory  *SearchStrategyFactory  // 检索策略工厂
    convertFactory *ConvertStrategyFactory // 转换策略工厂
    filterFactory  *FilterStrategyFactory  // 过滤策略工厂
}

// 执行流程:入参是“业务配置”和“ES查询条件”,出参是最终结果
func (p *DataProcessor) Process(config map[string]string, query map[string]interface{}) (interface{}, error) {
    // 1. 选检索策略:按config["data_type"](realtime/history)
    searcher := p.searchFactory.Create(config["data_type"])
    rawData, err := searcher.Search(query)
    if err != nil {
        return nil, fmt.Errorf("process search: %v", err)
    }

    // 2. 选转换策略:按config["output_type"](list/stat)
    convertor := p.convertFactory.Create(config["output_type"])
    convertedData, err := convertor.Convert(rawData)
    if err != nil {
        return nil, fmt.Errorf("process convert: %v", err)
    }

    // 3. 选过滤策略:按config["filter_rule"](sensitive/permission)
    filter := p.filterFactory.Create(config["filter_rule"])
    result, err := filter.Filter(convertedData)
    if err != nil {
        return nil, fmt.Errorf("process filter: %v", err)
    }

    return result, nil
}

实际调用场景

比如前端要查 “近 7 天的历史日志,用表格展示,且做权限过滤”,只需要传这样的配置:

go

运行

// 业务配置:查历史数据→用列表格式→做权限过滤
config := map[string]string{
    "data_type": "history",
    "output_type": "list",
    "filter_rule": "permission",
}

// ES查询条件:近7天的日志
query := map[string]interface{}{
    "query": map[string]interface{}{
        "range": map[string]interface{}{
            "create_time": map[string]interface{}{
                "gte": time.Now().AddDate(0, 0, -7).Unix(),
                "lte": time.Now().Unix(),
            },
        },
    },
}

// 执行流程
processor := initDataProcessor() // 初始化处理器(注入工厂依赖)
result, err := processor.Process(config, query)

四、为什么这种设计能解决问题?

用 “策略 + 工厂” 重构后,我们的代码彻底摆脱了之前的混乱,核心优势体现在三个方面:

1. 新增场景不用改旧代码(符合开闭原则)

比如现在要加一个 “地域过滤” 规则,只需要:

  1. 新建一个RegionFilter结构体,实现FilterStrategy接口;
  2. FilterStrategyFactoryCreate方法里加一个case "region": return &RegionFilter{}
  3. 调用时传filter_rule: "region"即可。

全程不用碰DataProcessor的流程代码,也不用改其他过滤策略 —— 完全符合 “对扩展开放,对修改关闭” 的设计原则。

2. 逻辑解耦,方便维护和测试

每个策略都是独立的:

  • 想改 “冷热 ES 的检索逻辑”,只动HotColdESSearcher
  • 想优化 “列表转换的字段”,只动ListConvertor
  • 测试时可以单独测某个策略,不用启动整个流程 —— 比如测SensitiveFilter,直接构造模拟数据调用Filter方法就行。

3. 灵活组合,适配复杂业务

如果后续需要 “同时做敏感词 + 权限过滤”,只需要加一个 “组合过滤策略”:

go

运行

// 组合过滤策略:同时执行多个子过滤
type CompositeFilter struct {
    filters []FilterStrategy // 子过滤策略列表
}

func (c *CompositeFilter) Filter(data interface{}) (interface{}, error) {
    var result interface{} = data
    // 依次执行所有子过滤
    for _, filter := range c.filters {
        var err error
        result, err = filter.Filter(result)
        if err != nil {
            return nil, err
        }
    }
    return result, nil
}

然后在工厂里创建组合策略:

go

运行

case "composite":
    return &CompositeFilter{
        filters: []FilterStrategy{
            &SensitiveFilter{...},
            &PermissionFilter{...},
        },
    }

不用改任何流程代码,就能实现策略的灵活组合 —— 这就是设计模式的魅力。

五、总结:什么时候该用 “策略 + 工厂”?

回顾我们的实战场景,当业务满足以下三个条件时,就可以考虑这种组合模式:

  1. 同一个流程步骤,有多种差异化实现(比如我们的 ES 检索有 2 种、转换有 2 种、过滤有 2 种);
  2. 需要根据外部条件动态选择实现(比如按 “数据类型”“输出格式” 选策略);
  3. 未来可能频繁新增实现(比如后续可能加新 ES 集群、新过滤规则)。

从 “满是 if-else 的混乱代码” 到 “可扩展的策略框架”,这个重构过程不仅解决了当下的问题,也为后续业务扩展铺平了路。其实设计模式不是 “花架子”,而是前人总结的 “解决特定问题的套路”—— 找准业务痛点,选对设计模式,代码会越写越轻松。