0%

设计模式之策略模式(Strategy)

一、动机

策略模式是一种行为设计模式,它能让你定义一系列算法,并将每种算法分别放入独立的类中,以使算法的对象能够相互替换。

二、解决方案

在软件构建过程中,某些对象使用的算法可能多种多样,经常改变,如果将这些算法都编码到对象中,将会使对象变得异常复杂;而且有时候支持不使用的算法也是一个性能负担。

如何在运行时根据需要透明地更改对象的算法?将算法与对象本身解耦,从而避免上述问题?

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

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

Context不负责选择符合任务需要的算法——客户端会将所需策略传递给Context。实际上,Context并不十分了解策略,它会通过同样的通用接口与所有策略进行交互,而该接口只需暴露一个方法来触发所选策略中封装的算法即可。

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

三、策略模式的结构

structure_of_Strategy

  1. 上下文Context)维护指向具体策略的引用,且仅通过策略接口与该对象进行交流。
  2. 策略Strategy)接口是所有具体策略的通用接口,它声明了一个上下文用于执行策略的方法。
  3. 具体策略Concrete Strategies)实现了上下文所用算法的各种不同变体。
  4. 当上下文需要运行算法时,它会在其已连接的策略对象上调用执行方法。上下文不清楚其所涉及的策略类型与算法的执行方式。
  5. 客户端Client)会创建一个特定策略对象并将其传递给上下文。上下文则会提供一个设置器以便客户端在运行时替换相关联的策略。
四、Strategy代码示例
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#include <string>
#include <iostream>

// 抽象策略,声明了不同策略所具有特定算法的接口
// Context 类会使用该接口来调用不同策略对应的算法
class Strategy {
public:
Strategy() {}
virtual ~Strategy() {}

// 策略接口
virtual void doAlgorithm() = 0;
};

// 具体策略1,遵循抽象策略接口所声明的算法规范
// 实现它们在上下文中的互换性
class ConcreteStrategy1 : public Strategy {
public:
ConcreteStrategy1() {}
~ConcreteStrategy1() {}

void doAlgorithm() {
std::cout << "ConcreteStrategy1's doAlgorithm!" << std::endl;
}
};

// 具体策略2
class ConcreteStrategy2 : public Strategy {
public:
ConcreteStrategy2() {}
~ConcreteStrategy2() {}

void doAlgorithm() {
std::cout << "ConcreteStrategy2's doAlgorithm!" << std::endl;
}
};

// 具体策略3
class ConcreteStrategy3 : public Strategy {
public:
ConcreteStrategy3() {}
~ConcreteStrategy3() {}

void doAlgorithm() {
std::cout << "ConcreteStrategy3's doAlgorithm!" << std::endl;
}
};

// 上下文定义了客户端关注的接口(绑定不同策略)
class Context {
// 上下文会维护指向某个策略对象的引用。它不知晓策略的具体类。
// 上下文必须通过策略接口来与所有策略进行交互。
Strategy *m_strategy;

public:
Context(Strategy *strategy) : m_strategy(strategy) {}
~Context() { delete m_strategy; }

// 上下文通常会通过构造函数来接收策略对象
// 同时还提供设置器以便在运行时切换策略
void setStrategy(Strategy *strategy) {
delete m_strategy;
m_strategy = strategy;
}

// 上下文会将一些工作委派给策略对象,而不是自行实现不同策略的算法。
void executeStrategy() {
return m_strategy->doAlgorithm();
}
};

// 客户端代码会选择具体策略并将其传递给上下文
// 客户端必须知晓策略之间的差异,才能做出正确的选择。
void clientCode() {
Context *context = new Context(new ConcreteStrategy1);
context->executeStrategy();

std::cout << "\n";

context->setStrategy(new ConcreteStrategy2);
context->executeStrategy();

std::cout << "\n";

context->setStrategy(new ConcreteStrategy3);
context->executeStrategy();
}

int main() {
clientCode();
}

Execution result:
ConcreteStrategy1's doAlgorithm!

ConcreteStrategy2's doAlgorithm!

ConcreteStrategy3's doAlgorithm!
五、使用场景
  • 你想使用对象中各种不同的算法变体, 并希望能在运行时切换算法时, 可使用策略模式。

策略模式让你能够将对象关联至可以不同方式执行特定子任务的不同子对象, 从而以间接方式在运行时更改对象行为。

  • 你有许多仅在执行某些行为时略有不同的相似类时, 可使用策略模式。

模式让你能将不同行为抽取到一个独立类层次结构中, 并将原始类组合成同一个, 从而减少重复代码。

  • 如果算法在上下文的逻辑中不是特别重要, 使用该模式能将类的业务逻辑与其算法实现细节隔离开来。

让你能将各种算法的代码、 内部数据和依赖关系与其他代码隔离开来。 不同客户端可通过一个简单接口执行算法, 并能在运行时进行切换。

  • 当类中使用了复杂条件运算符以在同一算法的不同变体中切换时, 可使用该模式。

策略模式将所有继承自同样接口的算法抽取到独立类中, 因此不再需要条件语句。 原始对象并不实现所有算法的变体, 而是将执行工作委派给其中的一个独立算法对象。

六、优缺点
  • 你可以在运行时切换对象内的算法。
  • 你可以将算法的实现和使用算法的代码隔离开来。
  • 你可以使用组合来代替继承。
  • 开闭原则。你无需对上下文进行修改就能够引入新的策略。
  • 如果你的算法极少发生改变,那么没有任何理由引入新的类和接口。使用该模式只会让程序过于复杂。
  • 客户端必须知晓策略间的不同——它需要选择合适的策略。
  • 许多现代编程语言支持函数类型功能,允许你在一组匿名函数中实现不同版本的算法。 这样,你使用这些函数的方式就和使用策略对象时完全相同,无需借助额外的类和接口来保持代码简洁。
七、与其它模式的关系
  • 桥接模式、状态模式和策略模式(在某种程度上包括适配器模式)模式的接口非常相似。实际上,它们都基于组合模式——即将工作委派给其他对象,不过也各自解决了不同的问题。模式并不只是以特定方式组织代码的配方,你还可以使用它们来和其他开发者讨论模式所解决的问题。
  • 命令模式和策略看上去很像,因为两者都能通过某些行为来参数化对象。但是,它们的意图有非常大的不同。
    • 你可以使用命令来将任何操作转换为对象。操作的参数将成为对象的成员变量。你可以通过转换来延迟操作的执行、将操作放入队列、保存历史命令或者向远程服务发送命令等。
    • 另一方面,策略通常可用于描述完成某件事的不同方式,让你能够在同一个上下文类中切换算法。
  • 装饰模式可让你更改对象的外表,策略则让你能够改变其本质。
  • 模板方法模式基于继承机制:它允许你通过扩展子类中的部分内容来改变部分算法。策略基于组合机制:你可以通过对相应行为提供不同的策略来改变对象的部分行为。模板方法在类层次上运作,因此它是静态的。策略在对象层次上运作,因此允许在运行时切换行为。
  • 状态可被视为策略的扩展。两者都基于组合机制:它们都通过将部分工作委派给 “帮手” 对象来改变其在不同情景下的行为。策略使得这些对象相互之间完全独立,它们不知道其他对象的存在。但状态模式没有限制具体状态之间的依赖,且允许它们自行改变在不同情景下的状态。