策略模式定义了算法家族,分别封装起来,让它们之间可以互相替换。此模式让算法的变化不会影响到使用算法的客户。

背景

开发一款导游程序,该程序的核心功能是提供美观的地图,以帮助用户在任何城市中快速定位。

用户期待的程序新功能是自动路线规划:他们希望输入地址后就能在地图上看到前往目的地的最快路线。

程序的首个版本只能规划公路路线,驾车旅行的人们对此非常满意。

  • 此后,又添加了规划公共交通路线的功能
  • 不久后,又要为骑行者规划路线
  • 又过了一段时间,又要为游览城市中的所有景点规划路线。

此时无论是修复简单缺陷还是微调街道权重,对某个算法进行任何修改都会影响整个类,从而增加在已有正常运行代码中引入错误的风险

在实现新功能的过程中,你的团队需要修改同一个巨大的类,这样他们所编写的代码相互之间就可能会出现冲突。

解决方案

策略模式建议找出负责用许多不同方式完成特定任务的类,然后将其中的算法抽取到一组被称为策略的独立类中。

名为上下文的原始类必须包含一个成员变量来存储对于每种策略的引用。上下文并不执行任务,而是将工作委派给已连接的策略对象。

上下文不负责选择符合任务需要的算法,客户端会将所需策略传递给上下文。

  • 实际上,上下文并不十分了解策略,它会通过同样的通用接口与所有策略进行交互,而该接口只需暴露一个方法来触发所选策略中封装的算法即可。

因此,上下文可独立于具体策略。这样就可在不修改上下文代码或其他策略的情况下添加新算法或修改已有算法了。

策略模式

单看策略模式,主要利用多态性。但策略模式往往不单独使用,它会和工厂模式配合使用。此时策略模式便可解耦策略的定义、创建、使用。

策略模式加工厂模式,能够达到去除if-else和switch的效果,主要靠工厂模式加持,借助于“查表法”找到指定策略进行使用。

适用场景

  • 想使用对象中各种不同的算法变体,并希望能在运行时切换算法时,可使用策略模式。
    • 策略模式让你能够将对象关联至可以不同方式执行特定子任务的不同子对象,从而以间接方式在运行时更改对象行为。
  • 当有许多仅在执行某些行为时略有不同的相似类时,可使用策略模式。
    • 策略模式能将不同行为抽取到一个独立类层次结构中,并将原始类组合成同一个,从而减少重复代码。
  • 如果算法在上下文的逻辑中不是特别重要,使用该模式能将类的业务逻辑与算法实现细节隔离开来。
    • 策略模式能将各种算法的代码、内部数据和依赖关系与其他代码隔离开来。不同客户端可通过一个简单接口执行算法,并能在运行时进行切换。
  • 当类中使用了复杂条件判断以在同一算法的不同变体中切换时,可使用该模式。
    • 策略模式将所有继承自同样接口的算法抽取到独立类中,因此不再需要条件语句。原始对象并不实现所有算法的变体,而是将执行工作委派给其中的一个独立算法对象。

实现步骤

  1. 从上下文类中找出修改频率较高的算法,也可能是用于在运行时选择某个算法变体的复杂条件判断逻辑。
  2. 声明该算法所有变体的通用策略接口。
  3. 将算法逐一抽取到各自的类中,它们都必须实现策略接口。
  4. 在上下文类中添加一个成员变量用于保存对策略对象的引用,然后提供设置器以修改该成员变量。
    • 上下文仅可通过策略接口同策略对象进行交互,如有需要还可定义一个接口来让策略访问其数据。
  5. 客户端必须将上下文类与相应策略进行关联,使上下文可以预期的方式完成其主要工作。

优缺点

优点:

  • 可以在运行时切换对象内的算法。
  • 可以将算法的实现和使用算法的代码隔离开来。
  • 可以使用组合来代替继承。
  • 开闭原则。 你无需对上下文进行修改就能够引入新的策略。

缺点:

  • 如果你的算法极少发生改变,那么没有任何理由引入新的类和接口。此时使用该模式只会让程序过于复杂。
  • 客户端必须知道策略间的不同,它需要选择合适的策略。
  • 许多现代编程语言支持函数类型功能, 允许在一组匿名函数中实现不同版本的算法。
    • 使用这些函数的方式就和使用策略对象时完全相同,无需借助额外的类和接口来保持代码简洁。

和其它模式的关系

  • 桥接模式状态模式策略模式(在某种程度上包括适配器模式)模式的接口非常相似。
    • 实际上,它们都基于组合模式——将工作委派给其他对象,不过也各自解决了不同的问题。
    • 模式并不只是以特定方式组织代码的配方,还可以使用它们来和其他开发者讨论模式所解决的问题。
  • 命令模式和策略看上去很像,因为两者都能通过某些行为来参数化对象 但是,它们的意图有非常大的不同。
    • 可以使用命令来将任何操作转换为对象。操作的参数将成为对象的成员变量。可以通过转换来延迟操作的执行、将操作放入队列、保存历史命令或者向远程服务发送命令等。
    • 策略通常可用于描述完成某件事的不同方式,让你能够在同一个上下文类中切换算法。
  • 装饰模式可让你更改对象的外表,策略则让你能够改变其本质。
  • 模板方法模式基于继承机制:它允许你通过扩展子类中的部分内容来改变部分算法。
    • 策略基于组合机制:你可以通过对相应行为提供不同的策略来改变对象的部分行为。
    • 模板方法在类层次上运作,因此它是静态的。
    • 策略在对象层次上运作,因此允许在运行时切换行为。
  • 状态模式可被视为策略的扩展。两者都基于组合机制:它们都通过将部分工作委派给 “帮手” 对象来改变其在不同情景下的行为。
    • 策略使得这些对象相互之间完全独立,它们不知道其他策略对象的存在。
    • 状态模式没有限制具体状态之间的依赖, 且允许它们自行改变在不同情景下的状态。

示例

进程内缓存可以通过多种算法实现,一些流行的算法有:

  • 最少最近使用(LRU):移除最近使用最少的一条条目。
  • 先进先出(FIFO):移除最早创建的条目。
  • 最少使用(LFU):移除使用频率最低一条条目。

问题在于

  • 如何将我们的缓存类与这些算法解耦,以便在运行时动态选择算法
  • 在添加新算法时,缓存类不应改变。

这就是策略模式发挥作用的场景。可以创建一系列的算法,每个算法都有自己的类。这些类中的每一个都遵循相同的接口,这使得系列算法之间可以互换。

假设通用接口名称为evictionAlgo,缓存类可以通过参数传递给evictionAlgo接口的驱逐函数中。缓存类将全部类型的驱逐算法委派给 evictionAlgo接口,而不是自行实现。

evictionAlgo是一个接口,可以在运行时将算法更改为LRU/FIFO/LFU等,而不需要对缓存类做出任何更改。

1
2
3
4
5
6
package main

type evictionAlgo interface {
	evict(c *cache)
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

type fifo struct {
}

func (f *fifo) evict(c *cache) {
	print("using fifo")
}

type lfu struct {
}

func (l *lfu) evict(c *cache) {
  print("using lfu")
}

type lru struct {
}

func (l *lru) evict(c *cache) {
  print("using lru")
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package main

// cache 非线程安全缓存,支持多种驱逐算法
type cache struct {
	storage      map[string]string
	evictionAlgo evictionAlgo
	capacity     int
	maxCapacity  int
}

// initCache 指定驱逐算法初始化缓存
func initCache(e evictionAlgo) *cache {
	storage := make(map[string]string)
	return &cache{
		storage:      storage,
		evictionAlgo: e,
		capacity:     0,
		maxCapacity:  2,
	}
}

// setEvictionAlgo 更新驱逐算法
func (c *cache) setEvictionAlgo(e evictionAlgo) {
	c.evictionAlgo = e
}

func (c *cache) evict() {
	c.evictionAlgo.evict(c)
	c.capacity--
}

func (c *cache) add(k, v string) {
	if c.capacity == c.maxCapacity {
		c.evict()
	}
	c.capacity++
	c.storage[k] = v
}

func (c *cache) get(key string) {
	delete(c.storage, key)
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

func main() {
	lfu := &lfu{}
	cache := initCache(lfu)

	cache.add("c", "3")

	lru := &lru{}
	cache.setEvictionAlgo(lru)
	cache.add("d", "4")

	fifo := &fifo{}
	cache.setEvictionAlgo(fifo)
	cache.add("e", "5")
}

策略模式很好的体现了开闭原则,也说明即使是很小的优化设计,也能给项目开发带来巨大的便利。

References

guru