你的 if-else 正在失控
一个支付系统,最初只支持支付宝。产品说要加微信支付,你加了一个 if。三个月后要加银联,又加了一个 if。半年后要加 Apple Pay、Google Pay、虚拟货币……等你回过神来,支付路由函数已经变成了这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
func Charge(channel string, amount int) error {
if channel == "alipay" {
// 40行支付宝逻辑
} else if channel == "wechat" {
// 35行微信逻辑
} else if channel == "unionpay" {
// 50行银联逻辑
} else if channel == "applepay" {
// 30行Apple Pay逻辑
} else {
return fmt.Errorf("unsupported channel: %s", channel)
}
return nil
}
|
这个函数的问题不止是长——它违反了开闭原则:每次新增渠道都要修改这个函数,而改老代码就有引入 bug 的风险。更糟的是,不同渠道的参数校验、签名算法、回调处理逻辑全揉在一起,测试时不得不 mock 所有的渠道。
策略模式就是解药。
一、策略模式:核心思想
策略模式的本质很朴素:把每个"做法"抽象成接口,把选择哪个做法推迟到运行时。
1
2
3
4
5
|
// PaymentStrategy 是所有支付渠道的抽象接口
type PaymentStrategy interface {
Pay(amount int, opts map[string]string) error
Name() string
}
|
每个渠道实现这个接口,各自管好自己的逻辑。上层代码只面向接口编程,不关心具体是哪个渠道。
二、从 if-else 到策略表:重构步骤
第一步:提取接口
先定义策略接口和渠道的参数结构:
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
|
type PaymentStrategy interface {
Pay(amount int, opts map[string]string) error
Name() string
}
// AlipayStrategy 支付宝策略
type AlipayStrategy struct {
AppID string
PrivKey string
}
func (s *AlipayStrategy) Pay(amount int, opts map[string]string) error {
// 1. 组装业务参数
params := map[string]string{
"app_id": s.AppID,
"amount": fmt.Sprintf("%d", amount),
"out_trade": opts["order_id"],
"timestamp": time.Now().Format("2006-01-02 15:04:05"),
}
// 2. 签名
sign := rsaSign(params, s.PrivKey)
params["sign"] = sign
// 3. 调用支付宝接口
return callAlipayGateway(params)
}
func (s *AlipayStrategy) Name() string { return "alipay" }
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// WechatStrategy 微信支付策略
type WechatStrategy struct {
MchID string
APIKey string
}
func (s *WechatStrategy) Pay(amount int, opts map[string]string) error {
// 微信支付自己的签名算法、自己的参数格式
body := wechatXMLBody(s.MchID, amount, opts["order_id"])
sign := wechatSign(body, s.APIKey)
return callWechatGateway(body, sign)
}
func (s *WechatStrategy) Name() string { return "wechat" }
|
注意:每个策略类是独立的文件,互不干扰,可以独立测试。
第二步:建注册表
有了策略实现,还需要一个"查找表"来根据渠道名找到对应策略。这里用一个简单的 map:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// StrategyRegistry 策略注册表
type StrategyRegistry struct {
strategies map[string]PaymentStrategy
}
func NewStrategyRegistry() *StrategyRegistry {
return &StrategyRegistry{
strategies: make(map[string]PaymentStrategy),
}
}
// Register 注册策略(并发安全)
func (r *StrategyRegistry) Register(s PaymentStrategy) {
r.strategies[s.Name()] = s
}
// Get 根据渠道名获取策略
func (r *StrategyRegistry) Get(name string) (PaymentStrategy, error) {
s, ok := r.strategies[name]
if !ok {
return nil, fmt.Errorf("strategy not found: %s", name)
}
return s, nil
}
|
第三步:组装与调用
在程序启动时注册所有渠道,运行时只需一次查询:
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
|
func main() {
registry := NewStrategyRegistry()
// 注册所有支付渠道——新增渠道只需在这里加一行
registry.Register(&AlipayStrategy{AppID: "2021xxx", PrivKey: "..."})
registry.Register(&WechatStrategy{MchID: "1900xxx", APIKey: "..."})
registry.Register(&UnionPayStrategy{MerchID: "777xxx"})
// 模拟一笔支付
ctx := &PaymentContext{Registry: registry}
err := ctx.Charge("alipay", 9900, map[string]string{"order_id": "ORD-001"})
if err != nil {
log.Fatal(err)
}
}
type PaymentContext struct {
Registry *StrategyRegistry
}
func (c *PaymentContext) Charge(channel string, amount int, opts map[string]string) error {
strategy, err := c.Registry.Get(channel)
if err != nil {
return err
}
// 公共逻辑:限流、日志、参数校验都放在这里
if amount <= 0 {
return errors.New("amount must be positive")
}
log.Printf("charging %d via %s, order=%s", amount, channel, opts["order_id"])
// 调用具体策略
return strategy.Pay(amount, opts)
}
|
重构后,Charge 方法再也不需要改了。新增支付渠道的步骤:
- 新建文件
unionpay_strategy.go
- 实现
PaymentStrategy 接口
- 在
main() 里注册
零修改老代码——这就是开闭原则的落地。
三、进阶:自动注册与依赖注入
手动注册虽然清晰,但渠道多了之后 main() 函数会越来越长。更优雅的做法是利用 Go 的 init() 机制实现自动注册:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// strategy_registry.go
package payment
var registry = NewStrategyRegistry()
func GetRegistry() *StrategyRegistry {
return registry
}
// alipay.go
package payment
func init() {
registry.Register(&AlipayStrategy{...})
}
// wechat.go
package payment
func init() {
registry.Register(&WechatStrategy{...})
}
|
只要 import 了这个包,所有 init() 自动执行,策略自动注册。用 goimports 或 wire 等依赖注入工具时,这个模式非常实用。
注意:init() 的执行顺序依赖文件名排序,不要在 init() 中依赖其他策略的注册状态。如果策略间有依赖,显式在 main() 中注册更安全。
四、策略模式的常见变体
变体一:带优先级的策略链
需要"按顺序尝试"的场景,比如限流降级链——先试本地令牌桶,再试 Redis 分布式限流,最后兜底放行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
type RateLimitStrategy interface {
TryAcquire(key string) (allowed bool, err error)
Priority() int
}
func (r *StrategyRegistry) AcquireChain(key string) error {
// 按优先级排序后依次尝试
strategies := r.sortedByPriority()
for _, s := range strategies {
allowed, err := s.TryAcquire(key)
if err == nil && allowed {
return nil // 命中即返回
}
}
return ErrRateLimited
}
|
变体二:组合策略(装饰器模式)
给策略加横切逻辑——比如所有渠道都需要加重试和监控——用装饰器包裹:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
type RetryDecorator struct {
inner PaymentStrategy
maxRetry int
}
func (d *RetryDecorator) Pay(amount int, opts map[string]string) error {
var lastErr error
for i := 0; i <= d.maxRetry; i++ {
if err := d.inner.Pay(amount, opts); err != nil {
lastErr = err
time.Sleep(time.Duration(1<<i) * time.Second) // 指数退避
continue
}
return nil
}
return lastErr
}
func (d *RetryDecorator) Name() string { return d.inner.Name() }
|
注册时套一层装饰器即可:
1
|
registry.Register(&RetryDecorator{inner: &AlipayStrategy{...}, maxRetry: 3})
|
五、什么时候不要用策略模式
策略模式虽好,但别过度设计。以下场景不需要:
- 分支数 ≤ 3 且不会增长:两个
if 就解决的问题,引入接口和注册表反而增加复杂度。
- 分支逻辑差异极小:如果各渠道只有参数不同、代码路径完全一样,用配置表比策略模式更简单——一个
map[string]Config 就够了。
- 分支不是"行为差异"而是"数据差异":比如根据地区显示不同汇率,那是个数据查询问题,不是策略问题。
记住设计模式的核心准则:先有重复,再消除重复。等你真的因为加渠道而改了三次 if-else,再重构不迟。
总结
| 场景 |
if-else |
策略模式 |
| 渠道数 < 3 |
✅ 够用 |
❌ 过度设计 |
| 渠道数 ≥ 3 且会增长 |
❌ 改不停 |
✅ 开闭原则 |
| 各渠道逻辑差异大 |
❌ 巨型函数 |
✅ 独立测试 |
| 需要运行时动态切换 |
❌ 不可能 |
✅ 注册表查找 |
重构的核心收获不是"用了策略模式",而是让每个渠道成为一个独立单元——独立开发、独立测试、独立部署。下次产品再提"加个渠道"时,你只需要 git checkout -b feature/new-channel,而不用战战兢兢地去碰那个巨型函数。