在做日志检索平台时,我们遇到了一个典型的业务痛点:系统里跑着两个 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. 新增场景不用改旧代码(符合开闭原则)
比如现在要加一个 “地域过滤” 规则,只需要:
- 新建一个
RegionFilter结构体,实现FilterStrategy接口; - 在
FilterStrategyFactory的Create方法里加一个case "region": return &RegionFilter{}; - 调用时传
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{...},
},
}
不用改任何流程代码,就能实现策略的灵活组合 —— 这就是设计模式的魅力。
五、总结:什么时候该用 “策略 + 工厂”?
回顾我们的实战场景,当业务满足以下三个条件时,就可以考虑这种组合模式:
- 同一个流程步骤,有多种差异化实现(比如我们的 ES 检索有 2 种、转换有 2 种、过滤有 2 种);
- 需要根据外部条件动态选择实现(比如按 “数据类型”“输出格式” 选策略);
- 未来可能频繁新增实现(比如后续可能加新 ES 集群、新过滤规则)。
从 “满是 if-else 的混乱代码” 到 “可扩展的策略框架”,这个重构过程不仅解决了当下的问题,也为后续业务扩展铺平了路。其实设计模式不是 “花架子”,而是前人总结的 “解决特定问题的套路”—— 找准业务痛点,选对设计模式,代码会越写越轻松。