策略模式
策略模式是一种行为设计模式, 它能让你定义一系列算法, 并将每种算法分别放入独立的类中, 以使算法的对象能够相互替换。
模式结构
- 上下文 (Context) 维护指向具体策略的引用, 且仅通过策略接口与该对象进行交流。
- 策略 (Strategy) 接口是所有具体策略的通用接口, 它声明了一个上下文用于执行策略的方法。
- 具体策略 (Concrete Strategies) 实现了上下文所用算法的各种不同变体。
- 当上下文需要运行算法时, 它会在其已连接的策略对象上调用执行方法。 上下文不清楚其所涉及的策略类型与算法的执行方式。
- 客户端 (Client) 会创建一个特定策略对象并将其传递给上下文。 上下文则会提供一个设置器以便客户端在运行时替换相关联的策略。
应用场景
-
当你想使用对象中各种不同的算法变体, 并希望能在运行时切换算法时, 可使用策略模式。
策略模式让你能够将对象关联至可以不同方式执行特定子任务的不同子对象, 从而以间接方式在运行时更改对象行为。
-
当你有许多仅在执行某些行为时略有不同的相似类时, 可使用策略模式。
策略模式让你能将不同行为抽取到一个独立类层次结构中, 并将原始类组合成同一个, 从而减少重复代码。
-
如果算法在上下文的逻辑中不是特别重要, 使用该模式能将类的业务逻辑与其算法实现细节隔离开来。
策略模式让你能将各种算法的代码、 内部数据和依赖关系与其他代码隔离开来。 不同客户端可通过一个简单接口执行算法, 并能在运行时进行切换。
-
当类中使用了复杂条件运算符以在同一算法的不同变体中切换时, 可使用该模式。
策略模式将所有继承自同样接口的算法抽取到独立类中, 因此不再需要条件语句。 原始对象并不实现所有算法的变体, 而是将执行工作委派给其中的一个独立算法对象。
实现方式
思考一下构建内存缓存的情形。 由于处在内存中, 故其大小会存在限制。 在达到其上限后, 一些条目就必须被移除以留出空间。 此类操作可通过多种算法进行实现。 一些流行的算法有:
- 最少最近使用 (LRU): 移除最近使用最少的一条条目。
- 先进先出 (FIFO): 移除最早创建的条目。
- 最少使用 (LFU): 移除使用频率最低一条条目。
问题在于如何将我们的缓存类与这些算法解耦, 以便在运行时更改算法。 此外, 在添加新算法时, 缓存类不应改变。
这就是策略模式发挥作用的场景。 可创建一系列的算法, 每个算法都有自己的类。 这些类中的每一个都遵循相同的接口, 这使得系列算法之间可以互换。 假设通用接口名称为 evictionAlgo
移除算法 。
现在, 我们的主要缓存类将嵌入至 evictionAlgo
接口中。 缓存类会将全部类型的移除算法委派给 evictionAlgo
接口, 而不是自行实现。 鉴于 evictionAlgo
是一个接口, 我们可在运行时将算法更改为 LRU、 FIFO 或者 LFU, 而不需要对缓存类做出任何更改。
- 从上下文类中找出修改频率较高的算法 (也可能是用于在运行时选择某个算法变体的复杂条件运算符)。
-
声明该算法所有变体的通用策略接口。
-
将算法逐一抽取到各自的类中, 它们都必须实现策略接口。
-
在上下文类中添加一个成员变量用于保存对于策略对象的引用。 然后提供设置器以修改该成员变量。 上下文仅可通过策略接口同策略对象进行交互, 如有需要还可定义一个接口来让策略访问其数据。
```go cache.go: 背景 package main
type Cache struct { storage map[string]string evictionAlgo EvictionAlgo capacity int maxCapacity int }
func initCache(e EvictionAlgo) *Cache { storage := make(map[string]string) return &Cache{ storage: storage, evictionAlgo: e, capacity: 0, maxCapacity: 2, } }
func (c *Cache) setEvictionAlgo(e EvictionAlgo) { c.evictionAlgo = e }
func (c *Cache) add(key, value string) { if c.capacity == c.maxCapacity { c.evict() } c.capacity++ c.storage[key] = value }
func (c *Cache) get(key string) { delete(c.storage, key) }
func (c *Cache) evict() { c.evictionAlgo.evict© c.capacity-- } ```
-
客户端必须将上下文类与相应策略进行关联, 使上下文可以预期的方式完成其主要工作。
优缺点
优点 | 缺点 |
---|---|
你可以在运行时切换对象内的算法。 | 如果你的算法极少发生改变, 那么没有任何理由引入新的类和接口。 使用该模式只会让程序过于复杂。 |
你可以将算法的实现和使用算法的代码隔离开来。 | 客户端必须知晓策略间的不同——它需要选择合适的策略。 |
你可以使用组合来代替继承。 | 许多现代编程语言支持函数类型功能, 允许你在一组匿名函数中实现不同版本的算法。 这样, 你使用这些函数的方式就和使用策略对象时完全相同, 无需借助额外的类和接口来保持代码简洁。 |
开闭原则。 你无需对上下文进行修改就能够引入新的策略。 |
与其他模式的关系
- 桥接模式、 状态模式 和 策略模式 (在某种程度上包括 适配器模式 ) 模式的接口非常相似。 实际上, 它们都基于 组合模式 ——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题。
- 命令模式 和 策略 看上去很像, 因为两者都能通过某些行为来参数化对象。 但是, 它们的意图有非常大的不同。
- 你可以使用命令来将任何操作转换为对象。 操作的参数将成为对象的成员变量。 你可以通过转换来延迟操作的执行、 将操作放入队列、 保存历史命令或者向远程服务发送命令等。
- 另一方面, 策略通常可用于描述完成某件事的不同方式, 让你能够在同一个上下文类中切换算法。
- 装饰模式 可让你更改对象的外表, 策略 则让你能够改变其本质。
- 模板方法模式 基于继承机制: 它允许你通过扩展子类中的部分内容来改变部分算法。 策略 基于 组合 机制: 你可以通过对相应行为提供不同的策略来改变对象的部分行为。 模板方法在类层次上运作, 因此它是静态的。 策略在对象层次上运作, 因此允许在运行时切换行为。
- 状态 可被视为 策略 的扩展。 两者都基于 组合 机制: 它们都通过将部分工作委派给 “帮手” 对象来改变其在不同情景下的行为。 策略使得这些对象相互之间完全独立, 它们不知道其他对象的存在。 但状态模式没有限制具体状态之间的依赖, 且允许它们自行改变在不同情景下的状态。