Appearance
策略模式
策略模式(Strategy)是行为型模式中最直观的一个。意图是:定义一系列算法,把它们一个个封装起来,使它们可以互相替换。策略模式让算法的变化独立于使用算法的客户端。在 Go 里,因为函数是一等公民,策略模式经常退化为「一个函数类型」,比 Java 的「接口 + 多个实现类」简洁得多。本章我们对比这两种写法,并说明策略与状态模式的区别。
一、策略模式意图:算法族封装
1. 意图
定义一系列算法,封装每一个,使它们可互换。策略模式让算法独立于使用它的客户端变化。
2. 经典场景
- 排序:不同排序算法(快排、归并、堆排)可互换。
- 支付:不同支付方式(支付宝、微信、银行卡)可互换。
- 折扣:不同促销策略(满减、打折、买赠)可互换。
- 路由:不同负载均衡策略(轮询、随机、加权)可互换。
- 压缩:不同压缩算法(gzip、zstd、lz4)可互换。
3. 结构
- Strategy 接口:定义算法接口。
- ConcreteStrategy:具体算法实现。
- Context:持有 Strategy 引用,把工作委托给策略。
二、Go 实现:接口 + 策略实现
1. 基本结构
go
package main
import "fmt"
// === 策略接口 ===
type DiscountStrategy interface {
Apply(price float64) float64
}
// === 具体策略 ===
type NoDiscount struct{}
func (NoDiscount) Apply(price float64) float64 { return price }
type PercentageDiscount struct {
Percent float64
}
func (p PercentageDiscount) Apply(price float64) float64 {
return price * (1 - p.Percent/100)
}
type FixedDiscount struct {
Amount float64
}
func (f FixedDiscount) Apply(price float64) float64 {
if price < f.Amount {
return 0
}
return price - f.Amount
}
type ThresholdDiscount struct {
Threshold float64
Reduce float64
}
func (t ThresholdDiscount) Apply(price float64) float64 {
if price >= t.Threshold {
return price - t.Reduce
}
return price
}
// === Context ===
type Cart struct {
strategy DiscountStrategy
}
func NewCart(s DiscountStrategy) *Cart {
return &Cart{strategy: s}
}
func (c *Cart) SetStrategy(s DiscountStrategy) {
c.strategy = s
}
func (c *Cart) Checkout(price float64) float64 {
return c.strategy.Apply(price)
}
func main() {
cart := NewCart(NoDiscount{})
fmt.Printf("原价: %.2f\n", cart.Checkout(120))
cart.SetStrategy(PercentageDiscount{Percent: 20})
fmt.Printf("8折: %.2f\n", cart.Checkout(120))
cart.SetStrategy(FixedDiscount{Amount: 30})
fmt.Printf("减30: %.2f\n", cart.Checkout(120))
cart.SetStrategy(ThresholdDiscount{Threshold: 100, Reduce: 20})
fmt.Printf("满100减20: %.2f\n", cart.Checkout(120))
}接口策略的优点:
- 策略可以有内部状态(如
PercentageDiscount.Percent)。 - 策略可以实现多个方法(不只是
Apply)。 - 易于扩展:新策略只要实现接口即可。
- 易于测试:可以 mock 策略。
三、函数作为策略(Go 特色)
Go 的函数是一等公民,可以赋值、传参、返回。如果策略只有一个方法且无复杂状态,用函数类型远比接口简洁。
1. 函数策略
go
package main
import "fmt"
// 策略就是一个函数类型
type DiscountFunc func(price float64) float64
// 各个具体策略就是函数
func NoDiscount(p float64) float64 { return p }
func PercentageDiscount(percent float64) DiscountFunc {
return func(p float64) float64 {
return p * (1 - percent/100)
}
}
func FixedDiscount(amount float64) DiscountFunc {
return func(p float64) float64 {
if p < amount {
return 0
}
return p - amount
}
}
func ThresholdDiscount(threshold, reduce float64) DiscountFunc {
return func(p float64) float64 {
if p >= threshold {
return p - reduce
}
return p
}
}
// Context 持有函数
type Cart struct {
strategy DiscountFunc
}
func NewCart(s DiscountFunc) *Cart { return &Cart{strategy: s} }
func (c *Cart) SetStrategy(s DiscountFunc) { c.strategy = s }
func (c *Cart) Checkout(price float64) float64 {
return c.strategy(price)
}
func main() {
cart := NewCart(NoDiscount)
fmt.Printf("原价: %.2f\n", cart.Checkout(120))
cart.SetStrategy(PercentageDiscount(20))
fmt.Printf("8折: %.2f\n", cart.Checkout(120))
cart.SetStrategy(FixedDiscount(30))
fmt.Printf("减30: %.2f\n", cart.Checkout(120))
cart.SetStrategy(ThresholdDiscount(100, 20))
fmt.Printf("满100减20: %.2f\n", cart.Checkout(120))
}对比接口版本,函数版本的优势:
- 代码量减少近一半。
PercentageDiscount(20)这种「工厂函数返回闭包」的写法,比PercentageDiscount{Percent: 20}还简洁。- 不需要定义接口、不需要写方法接收者。
2. 何时用函数、何时用接口
| 场景 | 推荐 |
|---|---|
| 策略只有一个方法 | 函数 |
| 策略有多个方法 | 接口 |
| 策略需要保存复杂状态 | 接口(结构体) |
| 策略需要生命周期(如 Close) | 接口 |
| 策略需要被 mock 测试 | 接口(更容易 mock) |
| 策略只是无状态计算 | 函数 |
| 公共 API,希望长期稳定 | 接口(更明确契约) |
Go 标准库的 sort.Slice 选择了函数:sort.Slice(s, func(i, j int) bool { ... })。http.Handler 选择了接口(虽然只有一个方法,但语义上是一个完整的处理器)。
四、策略模式 vs 状态模式
策略模式和行为型里另一个模式——状态模式(State)结构几乎一样:都是 Context 持有一个接口引用,运行时可切换。区别在 意图:
| 维度 | 策略模式 | 状态模式 |
|---|---|---|
| 意图 | 算法可互换 | 状态改变行为 |
| 谁决定切换 | 客户端主动选择 | 状态自己决定下一个状态 |
| 客户端是否知道策略存在 | 是 | 通常不感知 |
| 切换频率 | 偶尔切换 | 频繁切换 |
| 策略/状态间关系 | 互不引用 | 状态可能引用其他状态 |
举例:
- 「折扣策略」是策略——客户端根据用户选择切换,策略之间不互相依赖。
- 「订单状态机」是状态——待支付自动转已支付,状态转换有规则,状态自己知道下一个是谁。
简单判断:「我选」还是「它自己变」。前者是策略,后者是状态。下一章讲职责链时会再涉及状态机。
五、实战示例
1. 排序策略
Go 的 sort.Slice 本身就是策略模式的体现——传入不同的 less 函数得到不同的排序顺序:
go
package main
import (
"fmt"
"sort"
)
type User struct {
Name string
Age int
}
func main() {
users := []User{
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35},
}
// 策略 1:按年龄升序
sort.Slice(users, func(i, j int) bool {
return users[i].Age < users[j].Age
})
fmt.Println("按年龄升序:", users)
// 策略 2:按姓名降序
sort.Slice(users, func(i, j int) bool {
return users[i].Name > users[j].Name
})
fmt.Println("按姓名降序:", users)
// 策略 3:先按年龄,年龄相同按姓名
sort.Slice(users, func(i, j int) bool {
if users[i].Age != users[j].Age {
return users[i].Age < users[j].Age
}
return users[i].Name < users[j].Name
})
fmt.Println("复合排序:", users)
}把排序策略抽象成可复用的函数:
go
package main
import (
"fmt"
"sort"
)
type User struct {
Name string
Age int
}
// 把排序策略封装成函数,可复用
type UserSorter func(u1, u2 *User) bool
func ByAgeAsc(u1, u2 *User) bool { return u1.Age < u2.Age }
func ByAgeDesc(u1, u2 *User) bool { return u1.Age > u2.Age }
func ByNameAsc(u1, u2 *User) bool { return u1.Name < u2.Name }
func SortUsers(users []User, less UserSorter) {
sort.Slice(users, func(i, j int) bool {
return less(&users[i], &users[j])
})
}
func main() {
users := []User{
{"Alice", 30}, {"Bob", 25}, {"Charlie", 35},
}
SortUsers(users, ByAgeAsc)
fmt.Println("年龄升序:", users)
SortUsers(users, ByNameAsc)
fmt.Println("姓名升序:", users)
}2. 支付策略(接口版 + 注册表)
go
package main
import "fmt"
type PaymentStrategy interface {
Pay(amount float64) (string, error)
}
type Alipay struct{ AppID string }
type Wechat struct{ MchID string }
type BankCard struct{ CardNo string }
func (a Alipay) Pay(amount float64) (string, error) {
return fmt.Sprintf("Alipay(%s) 支付 %.2f", a.AppID, amount), nil
}
func (w Wechat) Pay(amount float64) (string, error) {
return fmt.Sprintf("Wechat(%s) 支付 %.2f", w.MchID, amount), nil
}
func (b BankCard) Pay(amount float64) (string, error) {
return fmt.Sprintf("BankCard(%s) 支付 %.2f", b.CardNo, amount), nil
}
// 支付上下文
type PaymentContext struct {
strategy PaymentStrategy
}
func (p *PaymentContext) SetStrategy(s PaymentStrategy) {
p.strategy = s
}
func (p *PaymentContext) Pay(amount float64) {
msg, err := p.strategy.Pay(amount)
if err != nil {
fmt.Println("支付失败:", err)
return
}
fmt.Println(msg)
}
func main() {
ctx := &PaymentContext{}
ctx.SetStrategy(Alipay{AppID: "2024"})
ctx.Pay(99.9)
ctx.SetStrategy(Wechat{MchID: "123"})
ctx.Pay(50.0)
ctx.SetStrategy(BankCard{CardNo: "6222****"})
ctx.Pay(1000.0)
}3. 折扣策略组合(高级)
策略可以组合:把多个策略串起来,依次应用。这是函数式策略的优势:
go
package main
import "fmt"
type DiscountFunc func(float64) float64
// 把多个策略串成一个
func Chain(discounts ...DiscountFunc) DiscountFunc {
return func(price float64) float64 {
for _, d := range discounts {
price = d(price)
}
return price
}
}
func PercentageOff(percent float64) DiscountFunc {
return func(p float64) float64 { return p * (1 - percent/100) }
}
func FixedOff(amount float64) DiscountFunc {
return func(p float64) float64 {
if p < amount {
return 0
}
return p - amount
}
}
func CapAt(max float64) DiscountFunc {
return func(p float64) float64 {
if p > max {
return max
}
return p
}
}
func main() {
// 复合策略:先 9 折,再减 20,最终价格不超过 500
combo := Chain(
PercentageOff(10),
FixedOff(20),
CapAt(500),
)
for _, price := range []float64{100, 300, 600, 1000} {
fmt.Printf("原价 %.2f -> 折后 %.2f\n", price, combo(price))
}
}这种「策略组合」在 Java 里要写 CompositeDiscount 类 + 一堆样板代码,Go 用函数一行搞定。
六、标准库中的应用
1. sort.Slice 的 less 函数
最经典的函数策略。sort.Slice、sort.SliceStable、sort.Search 都接受一个 less 或 f 函数作为策略。1.21 之后新增的 slices.SortFunc 也是同样思路,且支持泛型。
go
package main
import (
"fmt"
"slices"
)
func main() {
nums := []int{3, 1, 4, 1, 5, 9, 2, 6}
slices.SortFunc(nums, func(a, b int) int {
return a - b // 升序
})
fmt.Println("升序:", nums)
slices.SortFunc(nums, func(a, b int) int {
return b - a // 降序
})
fmt.Println("降序:", nums)
}2. http.Handler 与 http.HandlerFunc
http.Handler 是接口策略,http.HandlerFunc 是函数策略——Go 把两者统一了:函数类型 http.HandlerFunc 实现了 http.Handler 接口。这是 Go 中「函数策略」和「接口策略」融合的典范。
go
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
// HandlerFunc 是一个函数类型
type MyHandlerFunc func(string)
// 让函数类型实现「接口」(通过定义一个方法)
func (f MyHandlerFunc) Process(input string) {
f(input)
}
func main() {
// 直接用函数
var h MyHandlerFunc = func(s string) {
fmt.Println("处理:", s)
}
h.Process("hello")
// 标准 http 包同理
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
})
req := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
fmt.Println("http 响应:", w.Body.String())
}http.HandlerFunc 的妙处:你既可以写一个函数当 Handler,也可以写一个结构体(实现 ServeHTTP)当 Handler,两者可以互换。这是 Go 类型系统的精巧设计。
3. crypto/hash 的策略
hash.Hash 是接口,md5、sha1、sha256 都实现它。客户端可以无感切换哈希算法(前面工厂模式章节已展示)。
七、策略模式的取舍
1. 不要过度策略化
不是每个 if/else 都该改成策略模式。如果分支只有 2~3 个且不会扩展,直接 if/else 更清晰。策略模式适合:
- 分支多(4 个以上)。
- 分支可能扩展。
- 各分支逻辑差异大、独立。
- 需要运行时切换。
2. 策略选择逻辑的位置
「选哪个策略」的逻辑放哪里?三种做法:
- 客户端选:最简单,客户端自己
if决定传哪个策略。 - 工厂选:用一个工厂函数封装选择逻辑,客户端传一个 key。
- 配置驱动:从配置文件/数据库读策略名,用注册表查找。
go
package main
import "fmt"
type DiscountFunc func(float64) float64
var discountRegistry = map[string]DiscountFunc{
"none": func(p float64) float64 { return p },
"vip": func(p float64) float64 { return p * 0.8 },
"svip": func(p float64) float64 { return p * 0.7 },
}
func GetDiscount(name string) (DiscountFunc, bool) {
f, ok := discountRegistry[name]
return f, ok
}
func main() {
// 模拟从配置读取
cfg := "svip"
d, ok := GetDiscount(cfg)
if !ok {
fmt.Println("未知折扣")
return
}
fmt.Printf("最终价格: %.2f\n", d(200))
}3. 策略与依赖注入
策略模式本质是依赖注入的一种特例:把「算法」这个依赖注入到 Context。所以策略模式的测试性极好——注入 mock 策略即可测试 Context 的逻辑。
八、小结
- 策略模式把一族可互换的算法封装起来,使算法独立于客户端变化。
- Go 有两种实现:接口策略(
Strategy接口 + 多个实现)和函数策略(type Strategy func(...))。 - 函数策略是 Go 特色:当策略只有一个方法且无复杂状态时,用函数比接口简洁得多。
- 何时用接口:多方法、复杂状态、需生命周期管理、需 mock 测试、公共 API。何时用函数:单方法、无状态、内部使用。
- 策略 vs 状态:结构相似,意图不同。策略是「客户端选」,状态是「自己变」。
- 标准库中
sort.Slice/slices.SortFunc的 less 函数、http.Handler/http.HandlerFunc是策略模式的典范。 - 策略可以组合(Chain),这是函数式策略的独有优势。
- 不要过度策略化:分支少且不扩展时直接
if/else更清晰。
下一篇讲模板方法与命令模式,看 Go 在没有继承的情况下如何实现「算法骨架」。