软件设计

Swift 策略模式(Strategy Pattern):使用协议和闭包替换算法

你是否也遇到过每种支付方式都让 if-else 不断增加的代码?

4 分钟阅读
Swift 策略模式(Strategy Pattern):使用协议和闭包替换算法 封面图

你是否也遇到过每种支付方式都让 if-else 不断增加的代码?

当排序选项、折扣计算和过滤逻辑开始纠缠在一个巨大的 switch 语句中时,代码会越来越不敢修改。

这时就该用上 Swift 的策略模式了。

今天我们从实战角度讲解如何通过协议和闭包,像更换零件一样替换算法。

先来看核心要点。

策略模式将“做什么”和“怎么做”分离,使算法可以从外部替换。

在 Swift 中,可以通过协议(较重量级)或闭包(较轻量)两种方式实现。

下面逐一介绍。


什么是策略模式?先用 3 行总结

先搭好骨架,再深入复杂理论。

  1. 将需要替换的算法统一到一个公共规范(协议)中
  2. 将实际行为分别实现为策略对象或闭包
  3. 使用方只需了解规范,不必知道具体内容

可以把它理解为给游戏角色更换武器。

角色只需要知道“攻击”,无论是剑、弓还是魔法,都由装备的武器自行处理。

不修改角色代码,只更换武器。

这个“替换”基本就是策略模式的全部。


使用协议实现(经典方式)

先来看最教科书式的协议实现方式。

以折扣计算为例:普通会员、VIP 会员和使用优惠券等情况都有不同的计算方式。

下面的代码定义了所有策略必须遵守的公共规范,以及一个具体策略。

// 所有折扣策略都必须遵守的公共规范
protocol DiscountStrategy {
    func discount(for price: Int) -> Int
}

// VIP 策略: 20% 折扣
struct VIPDiscount: DiscountStrategy {
    func discount(for price: Int) -> Int { price * 20 / 100 }
}

接下来看看持有并使用该策略的一方。

使用方对象不关心传入的是哪种策略。

Checkout 只知道规范,不知道内部实现
Checkout 只知道规范,不知道内部实现
struct Checkout {
    var strategy: DiscountStrategy   // 可以替换策略的位置

    func finalPrice(_ price: Int) -> Int {
        price - strategy.discount(for: price)
    }
}

// 在支付时只替换策略
let cart = Checkout(strategy: VIPDiscount())

即使新增折扣政策,也不需要修改 Checkout

只需新建一个遵循 DiscountStrategy 的结构体即可。

不修改现有代码就能扩展功能,这就是实战中最明显的优势。


使用闭包实现(Swift 风格)

不过,策略明明很简单,却每次都创建结构体,确实有些麻烦。

连简单逻辑都用协议封装,反而容易让代码变得冗长。

这时在 Swift 中,闭包会更轻量、更自然。

struct Checkout {
    // 将策略本身作为函数接收
    var discount: (Int) -> Int

    func finalPrice(_ price: Int) -> Int {
        price - discount(price)
    }
}

// 在当前位置直接定义策略
let cart = Checkout(discount: { $0 * 20 / 100 })

无需声明额外类型,在传入时就可以直接写入逻辑。

对于排序条件、过滤条件和简单转换等轻量算法的替换,这种方式更加简洁。

sorted(by:) 原来也是我们每天使用的策略模式
sorted(by:) 原来也是我们每天使用的策略模式

实际上,标准库中的 sorted(by:) 正是这种基于闭包的策略模式。

排序的骨架保持不变,只通过闭包替换比较条件。

也就是说,我们早就在每天使用它了。


协议和闭包,什么时候该用哪个?

我把实战中的选择标准整理成了表格。

分类 协议方式 闭包方式
适用场景 策略复杂或持有状态 策略简短且简单
复用性 容易在多个地方复用 适合一次性使用
代码量 需要声明类型,代码较多 就地定义,代码较少
测试 易于作为独立类型验证 简单验证逻辑即可
可读性 通过名称表达意图 较短时清晰,较长时难读

我的判断标准是这样的。

如果策略拥有内部状态、会在多个页面复用,或需要通过名称清晰表达意图,就适合使用协议。

反之,如果只是临时替换一两行逻辑,闭包就是正确选择。

混合使用两者完全没有问题。我会将大型策略交给协议,将细节选项交给闭包。


常见问题(Q&A)

Q. 策略模式和继承(重写)有什么区别?

继承绑定在父类上,因此行为在编译时就被固定。

而策略模式可以在运行时自由替换策略,也不受类层次结构限制。

Q. 和使用 enum 的 switch 语句处理有什么区别?

使用 switch 时,每增加一个新分支,都必须打开并修改现有代码。

策略模式只需“添加”新策略,就能减少修改现有代码的情况。


归根结底,策略模式的核心只有一个:把变化的部分移到外部,让它更容易替换。

根据场景灵活使用今天学到的协议和闭包,你一定能摆脱令人厌烦的 if-else 地狱。先从一小段代码开始重构吧!

延伸阅读