简要介绍 过去出于学习目的写项目时,由于是“单兵作战”,所以完全不用考虑后期维护以及扩展成本,除了为了实践设计模式而刻意引入的架构外,大部分代码都是以快速实现为导向,怎么方便怎么写。
然鹅进入实际生产环境后我才发现,这业务迭代是真勾巴快,扩展性真的非常非常重要。
比如设想下面这个典型场景:我们刚刚完成了场景 A 和场景 B 的逻辑,结果没过多久 PM(产品经理) 又提过来一个需求增加一个新场景 C。场景 A、B、C 是同级别的,如果使用普通的硬编码分支,为了兼容新场景,我们必须侵入式地修改主函数,增加一个 else if 语句或者 case 语句。这样随着场景不断增多,代码会变得很脆弱,每次新增一个场景都要修改这个巨大的 if-else/switch-case,这显然违背了开闭原则 (对扩展开放,对修改关闭)。
简单来说,策略模式是一种行为型设计模式。它的核心思想是:定义一系列算法(或业务逻辑),把它们一个个单独封装起来,并且使它们可以互相替换。换句人话讲,就是把本来堆积在一个巨大 if-else 或 switch-case 里的各个业务分支,抽离成一个个独立的结构体。主流程不再关心具体的执行细节,只负责“根据当前上下文挑一个匹配的策略去执行”。
这样一来,主干逻辑就和具体的业务分支彻底解耦了。下次 PM 再提新场景,我们只需要默默建一个新文件实现接口即可,原有主干代码一行都不用动,这才是真正意义上的开闭原则。
接下来我以一个电商中非常常见的场景“根据用户会员等级进行订单折扣计算”为例,通过对比策略模式重构前和重构后的代码,优不优雅一眼便知。
重构前:充满硬编码 重构后:逻辑解耦
毋庸置疑,这种写法非常直观,所有逻辑都堆死在一个函数里,每种会员类型走什么逻辑一眼就能看出来。
虽说可以将每个具体逻辑作为一个函数解耦出去方便复用,但是每次增加会员类型时都要回来修改这个选择分支逻辑主函数,说不定一个手抖还把其他逻辑改崩了 😂。
代码示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package mainimport "fmt" type Order struct { Price float64 UserType string } func CalculatePrice (order Order) float64 { switch order.UserType { case "Normal" : return order.Price case "VIP" : return order.Price * 0.8 case "Coupon" : return order.Price - 10 default : return order.Price } } func main () { order := Order{Price: 100 , UserType: "VIP" } fmt.Printf("最终价格: %.2f\n" , CalculatePrice(order)) }
重构后虽然复杂性稍微有所上涨,但是可扩展性大大提高,核心函数 CalculatePriceOptimized 只有区区几行,即使未来 PM 要求增加“双十一大促”、“生日特价”等几十种新场景,核心函数也完全不需要改 😏。
代码示例 1 2 3 4 type DiscountStrategy interface { GetFinalPrice(price float64 ) float64 }
1 2 3 4 5 6 7 8 9 10 11 type NormalStrategy struct {}func (s *NormalStrategy) GetFinalPrice(price float64 ) float64 { return price }type VIPStrategy struct {}func (s *VIPStrategy) GetFinalPrice(price float64 ) float64 { return price * 0.8 }type CouponStrategy struct {}func (s *CouponStrategy) GetFinalPrice(price float64 ) float64 { return price - 10 }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package mainimport "fmt" var discountStrategies = map [string ]DiscountStrategy{ "Normal" : &NormalStrategy{}, "VIP" : &VIPStrategy{}, "Coupon" : &CouponStrategy{}, } func CalculatePriceOptimized (price float64 , userType string ) float64 { strategy, ok := discountStrategies[userType] if !ok { return price } return strategy.GetFinalPrice(price) } func main () { finalPrice := CalculatePriceOptimized(100 , "VIP" ) fmt.Printf("重构后的最终价格: %.2f\n" , finalPrice) }
动手实现 下面我们对上述策略模式的例子进行扩展,编写一个更加完整的例子,包括从请求校验到数据落库的完整流程。可以跟着敲一下,印象更加深刻。
首先介绍一下完整的代码结构和每个文件的作用。
首先我们来定义通用的请求和响应结构体。编写 strategy/context.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package strategytype Request struct { BizType string UserTag string EntityID int64 Operator string Meta map [string ]any } type Result struct { StrategyName string Success bool Message string Payload map [string ]any }
然后我们来定义接口,这是该策略模式的核心抽象,接口定义了每一个具体策略需要实现的方法,后续所有策略都要实现此接口,编写 strategy/interface.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package strategyimport "context" type Strategy interface { Name() string Priority() int Supports(ctx context.Context, req *Request) bool Validate(ctx context.Context, req *Request) error Init(ctx context.Context, req *Request) error CanExecute(ctx context.Context, req *Request) (bool , error ) BuildEntity(ctx context.Context, req *Request) (*Result, error ) Persist(ctx context.Context, req *Request, result *Result) error }
接下来我们添加优先级的默认实现,后续所有策略都可以继承该结构体,实现默认的优先级机制。继续编写 strategy/interface.go,在底部添加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type BaseStrategy struct { priority int } func NewBaseStrategy (priority int ) BaseStrategy { return BaseStrategy{priority: priority} } func (s BaseStrategy) Priority() int { return s.priority }
接下来编写注册策略、以及执行第一个匹配到的策略的逻辑。后续所有的策略只需要在 init 中调用 Register 函数将自己注册到 registry 策略库中,然后 ExecuteFirst 函数就能根据请求参数选择第一个匹配到的策略并执行策略的具体逻辑。编写 strategy/registry.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 package strategyimport ( "context" "fmt" "sort" ) var registry []Strategyfunc Register (s Strategy) { registry = append (registry, s) sort.SliceStable(registry, func (i, j int ) bool { return registry[i].Priority() > registry[j].Priority() }) } func ExecuteFirst (ctx context.Context, req *Request) (*Result, error ) { for _, s := range registry { if !s.Supports(ctx, req) { continue } if err := s.Validate(ctx, req); err != nil { return nil , fmt.Errorf("strategy %s validate failed: %w" , s.Name(), err) } if err := s.Init(ctx, req); err != nil { return nil , fmt.Errorf("strategy %s init failed: %w" , s.Name(), err) } ok, err := s.CanExecute(ctx, req) if err != nil { return nil , fmt.Errorf("strategy %s pre-check failed: %w" , s.Name(), err) } if !ok { return &Result{ StrategyName: s.Name(), Success: false , Message: "策略已命中,但当前条件不允许执行" , }, nil } result, err := s.BuildEntity(ctx, req) if err != nil { return nil , fmt.Errorf("strategy %s build entity failed: %w" , s.Name(), err) } if result == nil { return nil , fmt.Errorf("strategy %s return nil result" , s.Name()) } if err := s.Persist(ctx, req, result); err != nil { return nil , fmt.Errorf("strategy %s persist failed: %w" , s.Name(), err) } result.StrategyName = s.Name() result.Success = true return result, nil } return nil , fmt.Errorf("no strategy matched for bizType=%s userTag=%s" , req.BizType, req.UserTag) }
通用框架编写完了,接下来需要编写具体的策略类实现具体的逻辑。每个策略类只需要定义一个结构体并实现 Strategy 接口定义的所有方法,编写 New 函数创建该结构体的对象,在 init 方法中调用刚刚写好的 strategy.Register 函数将 New 创建的对象注册到策略库中等待被执行即可。编写 default.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 package strategiesimport ( "context" "fmt" "strategy_demo/strategy" ) func init () { strategy.Register(NewDefaultOrderStrategy()) } type DefaultOrderStrategy struct { strategy.BaseStrategy } func NewDefaultOrderStrategy () *DefaultOrderStrategy { return &DefaultOrderStrategy{ BaseStrategy: strategy.NewBaseStrategy(100 ), } } func (s *DefaultOrderStrategy) Name() string { return "default_order_strategy" } func (s *DefaultOrderStrategy) Supports(ctx context.Context, req *strategy.Request) bool { return req != nil && req.BizType == "order" } func (s *DefaultOrderStrategy) Validate(ctx context.Context, req *strategy.Request) error { if req == nil { return fmt.Errorf("req is nil" ) } if req.EntityID <= 0 { return fmt.Errorf("entityID must be greater than 0" ) } if req.Operator == "" { return fmt.Errorf("operator is empty" ) } return nil } func (s *DefaultOrderStrategy) Init(ctx context.Context, req *strategy.Request) error { if req.Meta == nil { req.Meta = map [string ]any{} } req.Meta["channel" ] = "standard" return nil } func (s *DefaultOrderStrategy) CanExecute(ctx context.Context, req *strategy.Request) (bool , error ) { return true , nil } func (s *DefaultOrderStrategy) BuildEntity(ctx context.Context, req *strategy.Request) (*strategy.Result, error ) { return &strategy.Result{ Message: "命中通用订单策略" , Payload: map [string ]any{ "entity_id" : req.EntityID, "channel" : req.Meta["channel" ], }, }, nil } func (s *DefaultOrderStrategy) Persist(ctx context.Context, req *strategy.Request, result *strategy.Result) error { if result.Payload == nil { result.Payload = map [string ]any{} } result.Payload["persisted" ] = true return nil }
编写 vip.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 package strategiesimport ( "context" "fmt" "strategy_demo/strategy" ) func init () { strategy.Register(NewVIPOrderStrategy()) } type VIPOrderStrategy struct { strategy.BaseStrategy } func NewVIPOrderStrategy () *VIPOrderStrategy { return &VIPOrderStrategy{ BaseStrategy: strategy.NewBaseStrategy(200 ), } } func (s *VIPOrderStrategy) Name() string { return "vip_order_strategy" } func (s *VIPOrderStrategy) Supports(ctx context.Context, req *strategy.Request) bool { return req != nil && req.BizType == "order" && req.UserTag == "vip" } func (s *VIPOrderStrategy) Validate(ctx context.Context, req *strategy.Request) error { if req == nil { return fmt.Errorf("req is nil" ) } if req.EntityID <= 0 { return fmt.Errorf("entityID must be greater than 0" ) } if req.Operator == "" { return fmt.Errorf("operator is empty" ) } return nil } func (s *VIPOrderStrategy) Init(ctx context.Context, req *strategy.Request) error { if req.Meta == nil { req.Meta = map [string ]any{} } req.Meta["channel" ] = "vip" req.Meta["discount" ] = "20%" return nil } func (s *VIPOrderStrategy) CanExecute(ctx context.Context, req *strategy.Request) (bool , error ) { return true , nil } func (s *VIPOrderStrategy) BuildEntity(ctx context.Context, req *strategy.Request) (*strategy.Result, error ) { return &strategy.Result{ Message: "命中 VIP 订单策略" , Payload: map [string ]any{ "entity_id" : req.EntityID, "channel" : req.Meta["channel" ], "discount" : req.Meta["discount" ], }, }, nil } func (s *VIPOrderStrategy) Persist(ctx context.Context, req *strategy.Request, result *strategy.Result) error { if result.Payload == nil { result.Payload = map [string ]any{} } result.Payload["persisted" ] = true result.Payload["audit_log" ] = "vip flow recorded" return nil }
编写 vip_v2.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 package strategiesimport ( "context" "fmt" "strategy_demo/strategy" ) func init () { strategy.Register(NewVIPOrderStrategyV2()) } type VIPOrderStrategyV2 struct { strategy.BaseStrategy } func NewVIPOrderStrategyV2 () *VIPOrderStrategyV2 { return &VIPOrderStrategyV2{ BaseStrategy: strategy.NewBaseStrategy(201 ), } } func (s *VIPOrderStrategyV2) Name() string { return "vip_order_strategy_v2" } func (s *VIPOrderStrategyV2) Supports(ctx context.Context, req *strategy.Request) bool { return req != nil && req.BizType == "order" && req.UserTag == "vip" } func (s *VIPOrderStrategyV2) Validate(ctx context.Context, req *strategy.Request) error { if req == nil { return fmt.Errorf("req is nil" ) } if req.EntityID <= 0 { return fmt.Errorf("entityID must be greater than 0" ) } if req.Operator == "" { return fmt.Errorf("operator is empty" ) } return nil } func (s *VIPOrderStrategyV2) Init(ctx context.Context, req *strategy.Request) error { if req.Meta == nil { req.Meta = map [string ]any{} } req.Meta["channel" ] = "vip" req.Meta["discount" ] = "20%" return nil } func (s *VIPOrderStrategyV2) CanExecute(ctx context.Context, req *strategy.Request) (bool , error ) { return true , nil } func (s *VIPOrderStrategyV2) BuildEntity(ctx context.Context, req *strategy.Request) (*strategy.Result, error ) { return &strategy.Result{ Message: "命中 VIP 订单策略" , Payload: map [string ]any{ "entity_id" : req.EntityID, "channel" : req.Meta["channel" ], "discount" : req.Meta["discount" ], }, }, nil } func (s *VIPOrderStrategyV2) Persist(ctx context.Context, req *strategy.Request, result *strategy.Result) error { if result.Payload == nil { result.Payload = map [string ]any{} } result.Payload["persisted" ] = true result.Payload["audit_log" ] = "vip flow recorded" return nil }
大功告成,接下来只需要编写入口函数,并模拟打过来的请求即可。有一个需要注意的点是需要导入 registries 策略包,这样策略类的 init 函数就会自动执行注册,非常方便,编写 main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package mainimport ( "context" "fmt" _ "strategy_demo/strategies" "strategy_demo/strategy" ) func main () { ctx := context.Background() requests := []*strategy.Request{ { BizType: "order" , UserTag: "vip" , EntityID: 1001 , Operator: "alice" , }, { BizType: "order" , UserTag: "normal" , EntityID: 1002 , Operator: "bob" , }, { BizType: "refund" , UserTag: "normal" , EntityID: 1003 , Operator: "charlie" , }, } for _, req := range requests { result, err := strategy.ExecuteFirst(ctx, req) if err != nil { fmt.Printf("请求 %+v 执行失败: %v\n" , *req, err) continue } fmt.Printf("请求 bizType=%s, userTag=%s 命中策略=%s, message=%s, payload=%v\n" , req.BizType, req.UserTag, result.StrategyName, result.Message, result.Payload) } }
执行试一下
非常完美!
分析 优点 一、入口层完全不关心业务类型
很明显业务入口只负责准备上下文,然后把选择权交给策略框架。类似上述编写的 main 函数,在正常 rpc 服务也是这样,主函数只负责解析请求,利用请求参数构造 strategy.Request 然后调用 strategy.ExecuteFirst 即可。入口层再也不用到处写 if bizType == xxx { if userTag == yyy { ... } else if userTag == zzz { ... } ... } else if ... 这种强耦合代码。
二、模版化、流程规范化
我们把每一种业务策略都定义为明确相同的流程:
Validate
Init
CanExecute
BuildEntity
Persist
这使得主流程非常稳定,每一部分的职责也更加清晰,后续的可维护性以及可扩展性都大大提高。
三、自动注册 + 优先级,让扩展变成新增文件而不用改老代码
每个具体策略实现都在 init() 里自动注册,注册方式统一是 strategy.Register(NewXXX),这使得新增一种业务类型或者将一种老类型升级为 v2 版本时,只要新增一个策略文件,主流程完全不用改。完美实践开闭原则。
四、用 Supports 做路由,用请求对象承载上下文
每个策略都标准化地实现 Supports,按业务类型或用户标识判断自己是否适配。比起分支语句,这样写也使得选择条件更加灵活,每个策略甚至能按需使用 context 请求上下文来判断自己是否符合,这对于灰度放量乃至环境测试都非常有用。
同时,这套代码把策略执行需要的参数集中塞进 strategy.Request 对象里,也使得流程之间更加连贯。
局限性 再看一下我们的标题,“使用策略模式重构复杂业务分支”,复杂 这两个字非常重要。如果我们的业务分支只有那么几个并且非常稳定,后续也几乎不会发生迭代,那我觉得使用 if-else 反而是一种心智负担更小的开发方式。
总结 总结一下,设计模式从来都不是银弹,更不是为了炫技。我们在写代码时,最忌讳的就是“拿着锤子找钉子”——业务模型极度简单时还强行套用模式,这只会让原本直白的逻辑变得支离破碎,徒增心智负担和系统的维护成本。
说到底,代码不仅是写给机器跑的,更是写给几个月后的自己、以及未来的协作者看的。一套优秀的代码架构,往往不是一开始就设计得多么宏大完美,而是在开发效率、系统复杂度和后期可维护性这三者之间,随着业务发展不断寻找那个最优雅 的平衡点。