软件设计

Strategy 模式 vs State 模式:结构一样,到底有什么不同?

Strategy 模式从外部选择算法,而 State 模式由当前状态驱动内部转换。这两个具有相同类结构的行为型模式,应根据意图和变更主体来区分。

4 分钟阅读
Strategy 模式 vs State 模式:结构一样,到底有什么不同? 封面图

学习设计模式时,几乎一定会遇到一个卡住的地方。

那就是 Strategy 模式和 State 模式。

把 UML(Unified Modeling Language,统一建模语言)图并排放在一起看,几乎像复制粘贴一样。一个接口、多个实现类,再由上下文持有它们。

你可能会想:“这不就是名字不同吗?”

先说结论。区分这两个模式的关键不是结构,而是意图。Strategy 从外部替换算法;State 则由内部的状态自行转移到下一个状态。

记住这一句话再往下读,就不会再混淆了。

使用状态对象移除实际条件判断的实现,会在 Swift State 模式重构中分步骤展开。

为什么它们的结构如此相似?

两者都是 GoF(Gang of Four)设计模式书中的行为型模式。

两者使用相同的材料:公共接口、多个实现类,以及持有它们的上下文。

所以只看代码,确实很难区分。

Strategy 模式写成代码是这样的。

protocol PaymentStrategy {
    func pay(_ amount: Int)
}

struct CardPayment: PaymentStrategy {
    func pay(_ amount: Int) { print("使用银行卡 \(amount)韩元付款") }
}

struct KakaoPay: PaymentStrategy {
    func pay(_ amount: Int) { print("使用 Kakao Pay \(amount)韩元付款") }
}

final class Checkout {
    var strategy: PaymentStrategy
    init(strategy: PaymentStrategy) { self.strategy = strategy }
    func pay(_ amount: Int) { strategy.pay(amount) }
}

let checkout = Checkout(strategy: CardPayment())
checkout.pay(10000)
checkout.strategy = KakaoPay()  // 从外部替换
checkout.pay(5000)
// 输出:
// 使用银行卡 10000韩元付款
// 使用 Kakao Pay 5000韩元付款

这里需要注意的是替换 strategy 的主体,也就是外部的客户端代码。

把支付方式从银行卡改成 Kakao Pay 的,是开发者,也就是外部的某个人。

Strategy 自己完全不知道接下来会是什么,也不关心。


那么 State 模式有什么不同?

State 模式由状态本身决定状态转换。

想想红绿灯。红灯知道下一个是绿灯,绿灯知道下一个是黄灯。

也就是说,转移到下一个状态的规则位于各个状态内部。

状态自行转移到下一个状态的流程,这就是 State 的本质
状态自行转移到下一个状态的流程,这就是 State 的本质
protocol TrafficState {
    func next(_ light: TrafficLight)
}

final class Red: TrafficState {
    func next(_ light: TrafficLight) {
        print("红 → 绿")
        light.state = Green()  // 状态自行指定下一个状态
    }
}

final class Green: TrafficState {
    func next(_ light: TrafficLight) {
        print("绿 → 黄")
        light.state = Yellow()
    }
}

final class Yellow: TrafficState {
    func next(_ light: TrafficLight) {
        print("黄 → 红")
        light.state = Red()
    }
}

final class TrafficLight {
    var state: TrafficState = Red()
    func change() { state.next(self) }
}

let light = TrafficLight()
light.change()
light.change()
light.change()
// 输出:
// 红 → 绿
// 绿 → 黄
// 黄 → 红

看出区别了吗?

在 Strategy 模式中,我们像 checkout.strategy = KakaoPay()一样从外部直接替换。

在 State 模式中,light.state = Green()由状态类自身完成。客户端只调用 change()。

这正是关键所在。

Strategy 从外部替换,State 在内部自行转移。


用三点总结决定性的区别

只用语言解释容易再次混淆,所以整理成表格。

分类 Strategy 模式 State 模式
意图 替换算法 根据状态改变行为
转换主体 外部客户端 状态对象自身
对象之间的关系 互不了解(独立) 相互了解并互相引用
生命周期 通常确定后保持不变 执行期间持续变化

请特别记住第二行和第三行。

各个 Strategy 互不了解彼此的存在。银行卡支付不需要知道 Kakao Pay。

而状态必须相互了解。因为红灯会直接创建并传递绿灯对象。

“是否相互了解”是从代码中区分这两个模式最可靠的线索。

把两段代码并排放在一起,就能看出“谁在改变”
把两段代码并排放在一起,就能看出“谁在改变”

什么时候该用,什么时候该避免?

我在实际工作中的判断标准可以总结如下。

情况 判断
只是完成同一件事的方法有多种时(排序、支付、压缩) Strategy 模式
对象会根据情况采取不同的行为,且情况会发生变化时 State 模式
转换规则变成复杂的 if-else 代码块时 用 State 模式解决
简单到只需传入一个函数时 两者都过度设计,使用闭包

还需要注意一点。

如果只有 2~3 个状态,转换也很简单,很多时候无需引入模式,使用 enum 和 switch 就够了。

模式是管理复杂度的工具,不是给简单代码披上形式的装饰。

面试时通常会这样问

Q. Strategy 模式和 State 模式结构相同,如何区分?

结构几乎相同,但意图不同。Strategy 的目的是从外部替换算法,State 的目的是根据内部状态改变行为。决定性的区别在于转换主体:Strategy 由客户端推进,State 则由状态对象自身转移到下一个状态。

Q. State 模式中状态之间会互相引用,这不会有问题吗?

状态之间确实会产生耦合。因此,也可以把转换逻辑从状态中移出,放到上下文或单独的转换表中,以降低耦合。当状态增多、转换相互交织时,也可以使用状态机库。


即使图看起来完全一样,也不用害怕。只要问“谁负责转换”,答案马上就会出现。

从外部替换就是 Strategy,在内部自行转移就是 State。记住这一句话,今天这篇文章就算达成目标了。下次再聊另一个容易混淆的模式。

延伸阅读