Contents

策略模式重构实战:用Go消除代码中泛滥的if-else

你的 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 方法再也不需要改了。新增支付渠道的步骤

  1. 新建文件 unionpay_strategy.go
  2. 实现 PaymentStrategy 接口
  3. 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() 自动执行,策略自动注册。用 goimportswire 等依赖注入工具时,这个模式非常实用。

注意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,而不用战战兢兢地去碰那个巨型函数。