一、为什么需要策略模式
你正在写一个数据导出模块,支持三种压缩格式:gzip、zlib、lz4。用户在配置里选一种,导出时用对应的算法压缩。第一版代码很直接:
func compress(data []byte, algo string) []byte { switch algo { case "gzip": // gzip 压缩逻辑,约 15 行 case "zlib": // zlib 压缩逻辑,约 15 行 case "lz4": // lz4 压缩逻辑,约 15 行 default: panic("unsupported algorithm: " + algo) }}看起来没什么问题。但需求在膨胀:产品要加 snappy 压缩,运维要加 zstd 压缩,安全团队要求某些场景必须用 AES 加密后再压缩。每加一种算法,你都要改这个 switch,而且 compress 函数已经从 50 行膨胀到 200 行。更麻烦的是,不同算法的初始化参数不同——gzip 有压缩级别,lz4 有块大小,zstd 有训练字典——这些参数的处理逻辑也全塞在 case 分支里。
问题不止于此。另一个模块也需要选择压缩算法,但它有自己的 switch。两处 switch 必须保持同步,漏改一个就是 bug。这就是散弹式修改(Shotgun Surgery):一个变化要改多个地方,每个地方都容易遗漏。
再看一个更常见的场景——排序。不同场景需要不同的排序规则:按价格升序、按销量降序、按评分和价格综合排序。你用 if-else 来选择排序逻辑:
function sort(items: Item[], rule: string): Item[] { if (rule === "price-asc") { return items.sort((a, b) => a.price - b.price); } else if (rule === "sales-desc") { return items.sort((a, b) => b.sales - a.sales); } else if (rule === "rating-price") { return items.sort((a, b) => (b.rating - a.rating) || (a.price - b.price)); } throw new Error(`unknown rule: ${rule}`);}每加一种排序规则,就要改这个函数。新增排序规则是业务常态,但每次都要修改已有代码——这违反了开闭原则(Open-Closed Principle):对扩展开放,对修改关闭。
策略模式解决的核心问题是:把算法的选择与算法的实现解耦。客户端不需要知道有哪些算法、怎么选择,只需要持有一个策略接口的引用,调用它就行。新增算法?写一个新策略类,注册进去,客户端代码一行不改。
二、现实类比
出行策略。从家到公司,你可以开车、坐公交、骑车或步行。目的地一样,但选择的策略不同——赶时间就开车,天气好就骑车,省钱就坐公交。你不需要为每种出行方式写一套独立的导航系统,只需要在出发前选一种策略,导航系统按策略规划路线。换策略也很简单:今天限号,把开车换成坐地铁,导航系统自动调整。
支付方式也是同理。买同一件商品,你可以刷信用卡、用支付宝、用微信支付。商品不关心你用什么方式付钱,它只关心「钱到账了」。支付策略可以随时切换——信用卡额度不够就换支付宝,支付宝余额不足就换微信。如果每加一种支付方式就要改商品结算逻辑,那电商系统早就崩溃了。
这两个类比的共同点:同一目标,多种实现,运行时可切换。这正是策略模式的适用场景。
三、核心思想
策略模式把一组算法封装成独立的策略类,它们实现同一个接口。客户端(Context)持有一个策略接口的引用,把算法执行委托给当前策略。需要换算法时,只需替换策略引用,客户端代码不变。
结构很清晰:Context 依赖 Strategy 接口,不依赖任何具体策略。ConcreteStrategyA、ConcreteStrategyB、ConcreteStrategyC 各自实现不同的算法。Context 通过 SetStrategy 切换策略,通过 Execute 委托执行。
3.1 消除条件分支
策略模式最直接的效果是用多态替代 switch/case。对比一下:
// 没有 Strategy:每次调用都要走 switchfunc compress(data []byte, algo string) []byte { switch algo { case "gzip": return compressGzip(data) case "zlib": return compressZlib(data) case "lz4": return compressLz4(data) } return nil}
// 有 Strategy:直接调用,多态分发func (c *Compressor) Compress(data []byte) []byte { return c.strategy.Compress(data) // 一行搞定}switch 版本每加一种算法就要改函数体,策略版本只需新增一个实现 Compress 接口的类型。新增算法不修改已有代码,只新增代码——这正是开闭原则的要求。
3.2 运行时策略切换
策略可以在运行时替换,这是它和继承的关键区别。如果用继承来实现算法变体,类在编译期就确定了,运行时无法切换。策略模式通过组合替代继承,Context 持有策略的引用而非策略的子类化,所以可以随时 SetStrategy 换一个:
compressor := NewCompressor(gzipStrategy)compressed := compressor.Compress(data) // 用 gzip
compressor.SetStrategy(lz4Strategy) // 运行时切换compressed = compressor.Compress(data) // 用 lz43.3 Go 标准库中的策略:sort.Interface
Go 的 sort.Sort 函数是策略模式的经典实现。它不关心你排序的是什么数据,只要求你提供三个方法:
type Interface interface { Len() int // 元素个数 Less(i, j int) bool // 比较规则 Swap(i, j int) // 交换元素}sort.Sort 是 Context,sort.Interface 是 Strategy。不同的 Less 实现就是不同的排序策略——按价格排、按销量排、按评分排,全靠 Less 的实现决定。sort.Sort 的排序算法(pdqsort)是固定的,但比较策略是可替换的。你不需要为每种排序规则写一个 SortByPrice、SortBySales 函数,只需要实现 Less 方法。
3.4 复杂度
| 属性 | 值 |
|---|---|
| 策略选择 | O(1)——接口调用,多态分发 |
| 算法执行 | 取决于具体策略的复杂度 |
| 新增策略 | O(1)——新增一个实现类,不改已有代码 |
| 策略切换 | O(1)——替换引用 |
策略模式本身的开销几乎为零——一次接口调用的间接寻址。真正的性能取决于你选的策略算法。
四、变体与对比
| 模式 | 核心区别 |
|---|---|
| 策略 vs 模板方法 | 策略用组合,运行时可换;模板方法用继承,骨架固定,子类只能覆盖特定步骤 |
| 策略 vs 命令 | 策略关注「怎么做」(算法选择),命令关注「做什么」(请求封装,支持撤销/队列) |
| 策略 vs 状态 | 状态模式的转移是自动的(由当前状态决定下一状态),策略由客户端显式设置 |
4.1 策略 vs 模板方法
两者都封装算法变体,但机制不同。模板方法用继承定义算法骨架,子类覆盖其中的步骤。策略用组合,把整个算法委托给策略对象。关键区别在于可替换性:模板方法的变体在编译期确定(你是什么子类就是什么行为),策略的变体在运行期可换。如果你的算法变体需要在运行时切换,选策略;如果算法骨架固定、只有个别步骤不同,选模板方法。
4.2 策略 vs 命令
策略和命令的接口签名可能很像——都接收参数、执行操作。但意图不同:策略是「选择算法」,命令是「封装请求」。命令模式的核心价值是请求的延迟执行、撤销、重做和队列化。策略模式没有这些需求,它只关心「用哪种算法」。一个简单的判断标准:如果你需要把操作存起来以后再执行,或者需要撤销,那是命令;如果你只是想在不同算法之间切换,那是策略。
4.3 策略 vs 状态
状态模式和策略模式的结构几乎一模一样——Context 持有接口引用,具体实现决定行为。区别在于谁控制切换:状态模式中,状态转移由状态对象自身决定(或由状态机规则驱动),客户端不知道也不关心当前是什么状态;策略模式中,策略由客户端显式选择和切换。状态模式的行为是「被驱动的」,策略模式的行为是「被选择的」。
4.4 依赖注入中的策略
Spring 框架大量使用策略模式,但用依赖注入来管理策略的选择。ResourceLoader 就是一个策略接口——DefaultResourceLoader 和 ServletContextResourceLoader 是两个具体策略。Spring 根据运行环境自动注入合适的实现,应用代码只依赖 ResourceLoader 接口。这本质上是策略模式 + 外部配置驱动策略选择,客户端连 SetStrategy 都不需要调。
4.5 函数式策略
在支持一等函数的语言里,策略模式可以更轻量。Go 的 sort.Slice 直接接收一个 less 函数,不需要定义完整的 sort.Interface:
sort.Slice(items, func(i, j int) bool { return items[i].Price < items[j].Price})TypeScript 也类似,策略就是一个函数:
type SortStrategy<T> = (a: T, b: T) => number;function sort<T>(items: T[], strategy: SortStrategy<T>): T[] { return items.sort(strategy);}函数式策略省去了接口定义和类声明,适合策略逻辑简单、不需要维护状态的场景。如果策略需要配置参数(比如压缩级别),用结构体或类更合适。
五、多语言实现
5.1 Go:压缩策略
// Compressor 是策略接口,定义压缩算法的契约type Compressor interface { Compress(data []byte) ([]byte, error) Name() string}
// GzipStrategy 实现 gzip 压缩type GzipStrategy struct { Level int // 压缩级别:1-9}
func (g *GzipStrategy) Compress(data []byte) ([]byte, error) { var buf bytes.Buffer w, err := gzip.NewWriterLevel(&buf, g.Level) if err != nil { return nil, err } if _, err := w.Write(data); err != nil { return nil, err } w.Close() return buf.Bytes(), nil}
func (g *GzipStrategy) Name() string { return "gzip" }
// Lz4Strategy 实现 lz4 压缩type Lz4Strategy struct { BlockSize int}
func (l *Lz4Strategy) Compress(data []byte) ([]byte, error) { // lz4 压缩实现,省略具体细节 dst := make([]byte, len(data)) n, err := lz4.CompressBlock(data, dst, nil) if err != nil { return nil, err } return dst[:n], nil}
func (l *Lz4Strategy) Name() string { return "lz4" }
// CompressionContext 持有策略引用,委托压缩执行type CompressionContext struct { strategy Compressor}
func NewCompressionContext(strategy Compressor) *CompressionContext { return &CompressionContext{strategy: strategy}}
// SetStrategy 运行时切换压缩策略func (c *CompressionContext) SetStrategy(strategy Compressor) { c.strategy = strategy}
func (c *CompressionContext) Compress(data []byte) ([]byte, error) { return c.strategy.Compress(data)}使用方式:
ctx := NewCompressionContext(&GzipStrategy{Level: 6})compressed, _ := ctx.Compress(data) // gzip 压缩
ctx.SetStrategy(&Lz4Strategy{BlockSize: 65536})compressed, _ = ctx.Compress(data) // 切换到 lz4新增压缩算法只需要实现 Compressor 接口,CompressionContext 不需要任何修改。
5.2 Go:sort.Interface 与函数式策略
Go 标准库的 sort.Interface 是策略模式的教科书实现。来看一个具体例子:
type Product struct { Name string Price float64 Sales int}
// ProductSorter 实现 sort.Interface,按指定字段排序type ProductSorter struct { products []Product less func(i, j int) bool // 策略:比较函数}
func (s *ProductSorter) Len() int { return len(s.products) }func (s *ProductSorter) Swap(i, j int) { s.products[i], s.products[j] = s.products[j], s.products[i] }func (s *ProductSorter) Less(i, j int) bool { return s.less(i, j) }
// 使用:不同的 less 函数就是不同的排序策略sort.Sort(&ProductSorter{ products: products, less: func(i, j int) bool { return products[i].Price < products[j].Price },})Go 1.8 之后提供了更简洁的函数式策略 sort.Slice,直接传入比较函数:
// 函数式策略:不需要定义类型,直接传 less 函数sort.Slice(products, func(i, j int) bool { return products[i].Sales > products[j].Sales})两种方式本质相同:sort.Sort 是经典的面向对象策略,sort.Slice 是函数式策略。选择哪种取决于策略是否需要复用——如果同一种排序逻辑在多处使用,定义一个 ProductSorter 更清晰;如果只用一次,sort.Slice 更简洁。
5.3 TypeScript:支付策略
// PaymentStrategy 定义支付策略的契约interface PaymentStrategy { pay(amount: number): Promise<PaymentResult>; name: string;}
interface PaymentResult { success: boolean; transactionId: string;}
// CreditCardStrategy 信用卡支付class CreditCardStrategy implements PaymentStrategy { name = "credit-card";
constructor( private cardNumber: string, private cvv: string, ) {}
async pay(amount: number): Promise<PaymentResult> { // 调用信用卡网关,省略具体实现 const txId = `CC-${Date.now()}`; return { success: true, transactionId: txId }; }}
// AlipayStrategy 支付宝支付class AlipayStrategy implements PaymentStrategy { name = "alipay";
constructor(private userId: string) {}
async pay(amount: number): Promise<PaymentResult> { // 调用支付宝 SDK,省略具体实现 const txId = `ALI-${Date.now()}`; return { success: true, transactionId: txId }; }}
// WeChatPayStrategy 微信支付class WeChatPayStrategy implements PaymentStrategy { name = "wechat";
constructor(private openId: string) {}
async pay(amount: number): Promise<PaymentResult> { // 调用微信支付 API,省略具体实现 const txId = `WX-${Date.now()}`; return { success: true, transactionId: txId }; }}
// PaymentContext 持有支付策略,委托支付执行class PaymentContext { private strategy: PaymentStrategy;
constructor(strategy: PaymentStrategy) { this.strategy = strategy; }
// 运行时切换支付方式 setStrategy(strategy: PaymentStrategy): void { this.strategy = strategy; }
async checkout(amount: number): Promise<PaymentResult> { return this.strategy.pay(amount); }}使用方式:
// 用户选了信用卡const payment = new PaymentContext( new CreditCardStrategy("4111****1111", "123"),);await payment.checkout(99.9);
// 用户切换到支付宝payment.setStrategy(new AlipayStrategy("user@example.com"));await payment.checkout(99.9);PaymentContext 不知道也不关心具体用了哪种支付方式,它只调用 strategy.pay()。新增支付方式——比如 Apple Pay——只需要新增一个实现 PaymentStrategy 的类,PaymentContext 和其他策略类都不需要改动。
六、生产验证
- C++ STL sort —— stl_algo.h#Lsort STL 的
std::sort接收一个可选的比较器(Comparator),默认是operator<。比较器就是排序策略——你可以传std::greater<T>实现降序,传自定义 lambda 实现多字段排序。STL 的所有算法(find_if、transform、accumulate)都用策略模式接收谓词和操作,这是策略模式在工业代码中最广泛的应用之一。 - Spring ResourceLoader —— ResourceLoader.java Spring 的
ResourceLoader接口定义了资源加载策略,DefaultResourceLoader处理 classpath 和文件系统资源,ServletContextResourceLoader处理 Web 应用资源。Spring 根据运行环境自动注入合适的实现,应用代码只依赖接口。这是策略模式 + 依赖注入的标准实践。 - Go sort.Interface —— sort.go Go 标准库的
sort.Sort函数接收sort.Interface,通过Len、Less、Swap三个方法抽象排序策略。从 Go 1.0 到现在,这个接口设计没有变过——策略模式把算法(pdqsort)和比较策略(Less)解耦,算法可以持续优化而不影响使用方。
七、小结
何时使用:
- 算法族互换——多种算法完成同一任务,需要在运行时选择或切换。压缩、加密、排序、序列化都是典型场景。
- 消除条件分支——
switch/case或if-else链根据某个标志选择不同行为,且分支可能增长。策略模式用多态替代条件分支,新增行为只加策略类。 - 避免散弹式修改——同一算法选择逻辑出现在多处,新增算法时所有地方都要改。策略模式把选择逻辑集中到一处。
- 算法需要独立配置——不同策略有不同的参数(压缩级别、块大小、超时时间),策略类可以封装各自的配置。
何时不用:
- 算法固定不变——如果只有一种算法且不会增加,策略模式是过度设计。直接写函数调用更简单。
- 策略极少切换——如果策略在程序启动时确定、运行中从不更换,用配置文件 + 工厂方法就够了,不需要策略模式的运行时切换能力。
- 策略之间差异极小——如果几个策略只有一两行不同,用模板方法或函数参数更合适,不必为两行代码建一个类。
- 客户端必须了解策略细节——策略模式的前提是客户端不需要知道策略的内部实现。如果客户端必须根据策略类型做不同处理,说明抽象不够,策略模式反而增加了复杂度。
八、参考资料
- GoF Design Patterns - Strategy - GoF 策略模式的原始定义
- Go sort package - Go 标准库排序包,
sort.Interface的官方文档 - Spring ResourceLoader - Spring 资源加载策略接口文档
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






