mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
3283 字
9 分钟
策略模式(Strategy Pattern)
2026-06-13

一、为什么需要策略模式#

你正在写一个数据导出模块,支持三种压缩格式: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)持有一个策略接口的引用,把算法执行委托给当前策略。需要换算法时,只需替换策略引用,客户端代码不变。

classDiagram class Context { -strategy: Strategy +SetStrategy(s: Strategy) +Execute() } class Strategy { <<interface>> +Execute(data) } class ConcreteStrategyA { +Execute(data) } class ConcreteStrategyB { +Execute(data) } class ConcreteStrategyC { +Execute(data) } Context --> Strategy : holds Strategy <|.. ConcreteStrategyA : implements Strategy <|.. ConcreteStrategyB : implements Strategy <|.. ConcreteStrategyC : implements

结构很清晰:Context 依赖 Strategy 接口,不依赖任何具体策略。ConcreteStrategyAConcreteStrategyBConcreteStrategyC 各自实现不同的算法。Context 通过 SetStrategy 切换策略,通过 Execute 委托执行。

3.1 消除条件分支#

策略模式最直接的效果是用多态替代 switch/case。对比一下:

// 没有 Strategy:每次调用都要走 switch
func 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) // 用 lz4

3.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)是固定的,但比较策略是可替换的。你不需要为每种排序规则写一个 SortByPriceSortBySales 函数,只需要实现 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 就是一个策略接口——DefaultResourceLoaderServletContextResourceLoader 是两个具体策略。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_iftransformaccumulate)都用策略模式接收谓词和操作,这是策略模式在工业代码中最广泛的应用之一。
  • Spring ResourceLoader —— ResourceLoader.java Spring 的 ResourceLoader 接口定义了资源加载策略,DefaultResourceLoader 处理 classpath 和文件系统资源,ServletContextResourceLoader 处理 Web 应用资源。Spring 根据运行环境自动注入合适的实现,应用代码只依赖接口。这是策略模式 + 依赖注入的标准实践。
  • Go sort.Interface —— sort.go Go 标准库的 sort.Sort 函数接收 sort.Interface,通过 LenLessSwap 三个方法抽象排序策略。从 Go 1.0 到现在,这个接口设计没有变过——策略模式把算法(pdqsort)和比较策略(Less)解耦,算法可以持续优化而不影响使用方。

七、小结#

何时使用

  • 算法族互换——多种算法完成同一任务,需要在运行时选择或切换。压缩、加密、排序、序列化都是典型场景。
  • 消除条件分支——switch/caseif-else 链根据某个标志选择不同行为,且分支可能增长。策略模式用多态替代条件分支,新增行为只加策略类。
  • 避免散弹式修改——同一算法选择逻辑出现在多处,新增算法时所有地方都要改。策略模式把选择逻辑集中到一处。
  • 算法需要独立配置——不同策略有不同的参数(压缩级别、块大小、超时时间),策略类可以封装各自的配置。

何时不用

  • 算法固定不变——如果只有一种算法且不会增加,策略模式是过度设计。直接写函数调用更简单。
  • 策略极少切换——如果策略在程序启动时确定、运行中从不更换,用配置文件 + 工厂方法就够了,不需要策略模式的运行时切换能力。
  • 策略之间差异极小——如果几个策略只有一两行不同,用模板方法或函数参数更合适,不必为两行代码建一个类。
  • 客户端必须了解策略细节——策略模式的前提是客户端不需要知道策略的内部实现。如果客户端必须根据策略类型做不同处理,说明抽象不够,策略模式反而增加了复杂度。

八、参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

策略模式(Strategy Pattern)
https://blog.souloss.com/posts/programming/behavioral/behavioral-strategy/
作者
Tsukimi
发布于
2026-06-13
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时