https://refactoringguru.cn/design-patterns/strategy

行为模式-策略模式

亦称: Strategy

意图

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

问题

一天, 你打算为游客们创建一款导游程序。 该程序的核心功能是提供美观的地图, 以帮助用户在任何城市中快速定位。

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

程序的首个版本只能规划公路路线。 驾车旅行的人们对此非常满意。 但很显然, 并非所有人都会在度假时开车。 因此你在下次更新时添加了规划步行路线的功能。 此后, 你又添加了规划公共交通路线的功能。

而这只是个开始。 不久后, 你又要为骑行者规划路线。 又过了一段时间, 你又要为游览城市中的所有景点规划路线。

导游代码将变得非常臃肿。

尽管从商业角度来看, 这款应用非常成功, 但其技术部分却让你非常头疼: 每次添加新的路线规划算法后, 导游应用中主要类的体积就会增加一倍。 终于在某个时候, 你觉得自己没法继续维护这堆代码了。

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

此外, 团队合作将变得低效。 如果你在应用成功发布后招募了团队成员, 他们会抱怨在合并冲突的工作上花费了太多时间。 在实现新功能的过程中, 你的团队需要修改同一个巨大的类, 这样他们所编写的代码相互之间就可能会出现冲突。

解决方案

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

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

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

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

路线规划策略。

在导游应用中, 每个路线规划算法都可被抽取到只有一个 build­Route生成路线方法的独立类中。 该方法接收起点和终点作为参数, 并返回路线中途点的集合。

即使传递给每个路径规划类的参数一模一样, 其所创建的路线也可能完全不同。 主要导游类的主要工作是在地图上渲染一系列中途点, 不会在意如何选择算法。 该类中还有一个用于切换当前路径规划策略的方法, 因此客户端 (例如用户界面中的按钮) 可用其他策略替换当前选择的路径规划行为。

真实世界类比

各种前往机场的出行策略

假如你需要前往机场。 你可以选择乘坐公共汽车、 预约出租车或骑自行车。 这些就是你的出行策略。 你可以根据预算或时间等因素来选择其中一种策略。

策略模式结构

  1. 上下文 (Context) 维护指向具体策略的引用, 且仅通过策略接口与该对象进行交流。
  2. 策略 (Strategy) 接口是所有具体策略的通用接口, 它声明了一个上下文用于执行策略的方法。
  3. 具体策略 (Concrete Strategies) 实现了上下文所用算法的各种不同变体。
  4. 当上下文需要运行算法时, 它会在其已连接的策略对象上调用执行方法。 上下文不清楚其所涉及的策略类型与算法的执行方式。
  5. 客户端 (Client) 会创建一个特定策略对象并将其传递给上下文。 上下文则会提供一个设置器以便客户端在运行时替换相关联的策略。

伪代码

在本例中, 上下文使用了多个策略来执行不同的计算操作。

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
// 策略接口声明了某个算法各个不同版本间所共有的操作。上下文会使用该接口来
// 调用有具体策略定义的算法。
interface Strategy is
method execute(a, b)

// 具体策略会在遵循策略基础接口的情况下实现算法。该接口实现了它们在上下文
// 中的互换性。
class ConcreteStrategyAdd implements Strategy is
method execute(a, b) is
return a + b

class ConcreteStrategySubtract implements Strategy is
method execute(a, b) is
return a - b

class ConcreteStrategyMultiply implements Strategy is
method execute(a, b) is
return a * b

// 上下文定义了客户端关注的接口。
class Context is
// 上下文会维护指向某个策略对象的引用。上下文不知晓策略的具体类。上下
// 文必须通过策略接口来与所有策略进行交互。
private strategy: Strategy

// 上下文通常会通过构造函数来接收策略对象,同时还提供设置器以便在运行
// 时切换策略。
method setStrategy(Strategy strategy) is
this.strategy = strategy

// 上下文会将一些工作委派给策略对象,而不是自行实现不同版本的算法。
method executeStrategy(int a, int b) is
return strategy.execute(a, b)


// 客户端代码会选择具体策略并将其传递给上下文。客户端必须知晓策略之间的差
// 异,才能做出正确的选择。
class ExampleApplication is
method main() is

创建上下文对象。

读取第一个数。
读取最后一个数。
从用户输入中读取期望进行的行为。

if (action == addition) then
context.setStrategy(new ConcreteStrategyAdd())

if (action == subtraction) then
context.setStrategy(new ConcreteStrategySubtract())

if (action == multiplication) then
context.setStrategy(new ConcreteStrategyMultiply())

result = context.executeStrategy(First number, Second number)

打印结果。

策略模式适合应用场景

当你想使用对象中各种不同的算法变体, 并希望能在运行时切换算法时, 可使用策略模式。

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

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

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

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

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

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

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

实现方式

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

策略模式优缺点

优点

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

缺点

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

与其他模式的关系

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

代码示例

策略是一种行为设计模式, 它将一组行为转换为对象, 并使其在原始上下文对象内部能够相互替换。

原始对象被称为上下文, 它包含指向策略对象的引用并将执行行为的任务分派给策略对象。 为了改变上下文完成其工作的方式, 其他对象可以使用另一个对象来替换当前链接的策略对象。

在 C# 中使用模式

复杂度: ★☆☆

流行度: ★★★

使用示例: 策略模式在 C# 代码中很常见。 它经常在各种框架中使用, 能在不扩展类的情况下向用户提供改变其行为的方式。

识别方法: 策略模式可以通过允许嵌套对象完成实际工作的方法以及允许将该对象替换为不同对象的设置器来识别。

概念示例

本例说明了策略设计模式的结构并重点回答了下面的问题:

  • 它由哪些类组成?
  • 这些类扮演了哪些角色?
  • 模式中的各个元素会以何种方式相互关联?

Program.cs: 概念示例

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
99
100
101
102
103
using System;
using System.Collections.Generic;

namespace RefactoringGuru.DesignPatterns.Strategy.Conceptual
{
// The Context defines the interface of interest to clients.
class Context
{
// The Context maintains a reference to one of the Strategy objects. The
// Context does not know the concrete class of a strategy. It should
// work with all strategies via the Strategy interface.
private IStrategy _strategy;

public Context()
{ }

// Usually, the Context accepts a strategy through the constructor, but
// also provides a setter to change it at runtime.
public Context(IStrategy strategy)
{
this._strategy = strategy;
}

// Usually, the Context allows replacing a Strategy object at runtime.
public void SetStrategy(IStrategy strategy)
{
this._strategy = strategy;
}

// The Context delegates some work to the Strategy object instead of
// implementing multiple versions of the algorithm on its own.
public void DoSomeBusinessLogic()
{
Console.WriteLine("Context: Sorting data using the strategy (not sure how it'll do it)");
var result = this._strategy.DoAlgorithm(new List<string> { "a", "b", "c", "d", "e" });

string resultStr = string.Empty;
foreach (var element in result as List<string>)
{
resultStr += element + ",";
}

Console.WriteLine(resultStr);
}
}

// The Strategy interface declares operations common to all supported
// versions of some algorithm.
//
// The Context uses this interface to call the algorithm defined by Concrete
// Strategies.
public interface IStrategy
{
object DoAlgorithm(object data);
}

// Concrete Strategies implement the algorithm while following the base
// Strategy interface. The interface makes them interchangeable in the
// Context.
class ConcreteStrategyA : IStrategy
{
public object DoAlgorithm(object data)
{
var list = data as List<string>;
list.Sort();

return list;
}
}

class ConcreteStrategyB : IStrategy
{
public object DoAlgorithm(object data)
{
var list = data as List<string>;
list.Sort();
list.Reverse();

return list;
}
}

class Program
{
static void Main(string[] args)
{
// The client code picks a concrete strategy and passes it to the
// context. The client should be aware of the differences between
// strategies in order to make the right choice.
var context = new Context();

Console.WriteLine("Client: Strategy is set to normal sorting.");
context.SetStrategy(new ConcreteStrategyA());
context.DoSomeBusinessLogic();

Console.WriteLine();

Console.WriteLine("Client: Strategy is set to reverse sorting.");
context.SetStrategy(new ConcreteStrategyB());
context.DoSomeBusinessLogic();
}
}
}

Output.txt: 执行结果

1
2
3
4
5
6
7
Client: Strategy is set to normal sorting.
Context: Sorting data using the strategy (not sure how it'll do it)
a,b,c,d,e

Client: Strategy is set to reverse sorting.
Context: Sorting data using the strategy (not sure how it'll do it)
e,d,c,b,a

在 C++ 中使用模式

复杂度: ★☆☆

流行度: ★★★

使用示例: 策略模式在 C++ 代码中很常见。 它经常在各种框架中使用, 能在不扩展类的情况下向用户提供改变其行为的方式。

识别方法: 策略模式可以通过允许嵌套对象完成实际工作的方法以及允许将该对象替换为不同对象的设置器来识别。

概念示例

本例说明了策略设计模式的结构并重点回答了下面的问题:

  • 它由哪些类组成?
  • 这些类扮演了哪些角色?
  • 模式中的各个元素会以何种方式相互关联?

main.cc: 概念示例

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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
/**
* The Strategy interface declares operations common to all supported versions
* of some algorithm.
*
* The Context uses this interface to call the algorithm defined by Concrete
* Strategies.
*/
class Strategy
{
public:
virtual ~Strategy() {}
virtual std::string DoAlgorithm(const std::vector<std::string> &data) const = 0;
};

/**
* The Context defines the interface of interest to clients.
*/

class Context
{
/**
* @var Strategy The Context maintains a reference to one of the Strategy
* objects. The Context does not know the concrete class of a strategy. It
* should work with all strategies via the Strategy interface.
*/
private:
Strategy *strategy_;
/**
* Usually, the Context accepts a strategy through the constructor, but also
* provides a setter to change it at runtime.
*/
public:
Context(Strategy *strategy = nullptr) : strategy_(strategy)
{
}
~Context()
{
delete this->strategy_;
}
/**
* Usually, the Context allows replacing a Strategy object at runtime.
*/
void set_strategy(Strategy *strategy)
{
delete this->strategy_;
this->strategy_ = strategy;
}
/**
* The Context delegates some work to the Strategy object instead of
* implementing +multiple versions of the algorithm on its own.
*/
void DoSomeBusinessLogic() const
{
// ...
std::cout << "Context: Sorting data using the strategy (not sure how it'll do it)\n";
std::string result = this->strategy_->DoAlgorithm(std::vector<std::string>{"a", "e", "c", "b", "d"});
std::cout << result << "\n";
// ...
}
};

/**
* Concrete Strategies implement the algorithm while following the base Strategy
* interface. The interface makes them interchangeable in the Context.
*/
class ConcreteStrategyA : public Strategy
{
public:
std::string DoAlgorithm(const std::vector<std::string> &data) const override
{
std::string result;
std::for_each(std::begin(data), std::end(data), [&result](const std::string &letter) {
result += letter;
});
std::sort(std::begin(result), std::end(result));

return result;
}
};
class ConcreteStrategyB : public Strategy
{
std::string DoAlgorithm(const std::vector<std::string> &data) const override
{
std::string result;
std::for_each(std::begin(data), std::end(data), [&result](const std::string &letter) {
result += letter;
});
std::sort(std::begin(result), std::end(result));
for (int i = 0; i < result.size() / 2; i++)
{
std::swap(result[i], result[result.size() - i - 1]);
}

return result;
}
};
/**
* The client code picks a concrete strategy and passes it to the context. The
* client should be aware of the differences between strategies in order to make
* the right choice.
*/

void ClientCode()
{
Context *context = new Context(new ConcreteStrategyA);
std::cout << "Client: Strategy is set to normal sorting.\n";
context->DoSomeBusinessLogic();
std::cout << "\n";
std::cout << "Client: Strategy is set to reverse sorting.\n";
context->set_strategy(new ConcreteStrategyB);
context->DoSomeBusinessLogic();
delete context;
}

int main()
{
ClientCode();
return 0;
}

Output.txt: 执行结果

1
2
3
4
5
6
7
Client: Strategy is set to normal sorting.
Context: Sorting data using the strategy (not sure how it'll do it)
abcde

Client: Strategy is set to reverse sorting.
Context: Sorting data using the strategy (not sure how it'll do it)
edcba

在 Java 中使用模式

复杂度: ★☆☆

流行度: ★★★

使用示例: 策略模式在 Java 代码中很常见。 它经常在各种框架中使用, 能在不扩展类的情况下向用户提供改变其行为的方式。

Java 8 开始支持 lambda 方法, 它可作为一种替代策略模式的简单方式。

这里有一些核心 Java 程序库中策略模式的示例:

识别方法: 策略模式可以通过允许嵌套对象完成实际工作的方法以及允许将该对象替换为不同对象的设置器来识别。

电子商务应用中的支付方法

在本例中, 策略模式被用于在电子商务应用中实现各种支付方法。 客户选中希望购买的商品后需要选择一种支付方式: Paypal 或者信用卡。

具体策略不仅会完成实际的支付工作, 还会改变支付表单的行为, 并在表单中提供相应的字段来记录支付信息。

strategies

strategies/PayStrategy.java: 通用的支付方法接口

1
2
3
4
5
6
7
8
9
package refactoring_guru.strategy.example.strategies;

/**
* Common interface for all strategies.
*/
public interface PayStrategy {
boolean pay(int paymentAmount);
void collectPaymentDetails();
}

strategies/PayByPayPal.java: 使用 PayPal 支付

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
package refactoring_guru.strategy.example.strategies;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;

/**
* Concrete strategy. Implements PayPal payment method.
*/
public class PayByPayPal implements PayStrategy {
private static final Map<String, String> DATA_BASE = new HashMap<>();
private final BufferedReader READER = new BufferedReader(new InputStreamReader(System.in));
private String email;
private String password;
private boolean signedIn;

static {
DATA_BASE.put("amanda1985", "amanda@ya.com");
DATA_BASE.put("qwerty", "john@amazon.eu");
}

/**
* Collect customer's data.
*/
@Override
public void collectPaymentDetails() {
try {
while (!signedIn) {
System.out.print("Enter the user's email: ");
email = READER.readLine();
System.out.print("Enter the password: ");
password = READER.readLine();
if (verify()) {
System.out.println("Data verification has been successful.");
} else {
System.out.println("Wrong email or password!");
}
}
} catch (IOException ex) {
ex.printStackTrace();
}
}

private boolean verify() {
setSignedIn(email.equals(DATA_BASE.get(password)));
return signedIn;
}

/**
* Save customer data for future shopping attempts.
*/
@Override
public boolean pay(int paymentAmount) {
if (signedIn) {
System.out.println("Paying " + paymentAmount + " using PayPal.");
return true;
} else {
return false;
}
}

private void setSignedIn(boolean signedIn) {
this.signedIn = signedIn;
}
}

strategies/PayByCreditCard.java: 使用信用卡支付

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
package refactoring_guru.strategy.example.strategies;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

/**
* Concrete strategy. Implements credit card payment method.
*/
public class PayByCreditCard implements PayStrategy {
private final BufferedReader READER = new BufferedReader(new InputStreamReader(System.in));
private CreditCard card;

/**
* Collect credit card data.
*/
@Override
public void collectPaymentDetails() {
try {
System.out.print("Enter the card number: ");
String number = READER.readLine();
System.out.print("Enter the card expiration date 'mm/yy': ");
String date = READER.readLine();
System.out.print("Enter the CVV code: ");
String cvv = READER.readLine();
card = new CreditCard(number, date, cvv);

// Validate credit card number...

} catch (IOException ex) {
ex.printStackTrace();
}
}

/**
* After card validation we can charge customer's credit card.
*/
@Override
public boolean pay(int paymentAmount) {
if (cardIsPresent()) {
System.out.println("Paying " + paymentAmount + " using Credit Card.");
card.setAmount(card.getAmount() - paymentAmount);
return true;
} else {
return false;
}
}

private boolean cardIsPresent() {
return card != null;
}
}

strategies/CreditCard.java: 信用卡类

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
package refactoring_guru.strategy.example.strategies;

/**
* Dummy credit card class.
*/
public class CreditCard {
private int amount;
private String number;
private String date;
private String cvv;

CreditCard(String number, String date, String cvv) {
this.amount = 100_000;
this.number = number;
this.date = date;
this.cvv = cvv;
}

public void setAmount(int amount) {
this.amount = amount;
}

public int getAmount() {
return amount;
}
}

order/Order.java: 订单类

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
package refactoring_guru.strategy.example.order;

import refactoring_guru.strategy.example.strategies.PayStrategy;

/**
* Order class. Doesn't know the concrete payment method (strategy) user has
* picked. It uses common strategy interface to delegate collecting payment data
* to strategy object. It can be used to save order to database.
*/
public class Order {
private int totalCost = 0;
private boolean isClosed = false;

public void processOrder(PayStrategy strategy) {
strategy.collectPaymentDetails();
// Here we could collect and store payment data from the strategy.
}

public void setTotalCost(int cost) {
this.totalCost += cost;
}

public int getTotalCost() {
return totalCost;
}

public boolean isClosed() {
return isClosed;
}

public void setClosed() {
isClosed = true;
}
}

Demo.java: 客户端代码

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
package refactoring_guru.strategy.example;

import refactoring_guru.strategy.example.order.Order;
import refactoring_guru.strategy.example.strategies.PayByCreditCard;
import refactoring_guru.strategy.example.strategies.PayByPayPal;
import refactoring_guru.strategy.example.strategies.PayStrategy;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;

/**
* World first console e-commerce application.
*/
public class Demo {
private static Map<Integer, Integer> priceOnProducts = new HashMap<>();
private static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
private static Order order = new Order();
private static PayStrategy strategy;

static {
priceOnProducts.put(1, 2200);
priceOnProducts.put(2, 1850);
priceOnProducts.put(3, 1100);
priceOnProducts.put(4, 890);
}

public static void main(String[] args) throws IOException {
while (!order.isClosed()) {
int cost;

String continueChoice;
do {
System.out.print("Please, select a product:" + "\n" +
"1 - Mother board" + "\n" +
"2 - CPU" + "\n" +
"3 - HDD" + "\n" +
"4 - Memory" + "\n");
int choice = Integer.parseInt(reader.readLine());
cost = priceOnProducts.get(choice);
System.out.print("Count: ");
int count = Integer.parseInt(reader.readLine());
order.setTotalCost(cost * count);
System.out.print("Do you wish to continue selecting products? Y/N: ");
continueChoice = reader.readLine();
} while (continueChoice.equalsIgnoreCase("Y"));

if (strategy == null) {
System.out.println("Please, select a payment method:" + "\n" +
"1 - PalPay" + "\n" +
"2 - Credit Card");
String paymentMethod = reader.readLine();

// Client creates different strategies based on input from user,
// application configuration, etc.
if (paymentMethod.equals("1")) {
strategy = new PayByPayPal();
} else {
strategy = new PayByCreditCard();
}
}

// Order object delegates gathering payment data to strategy object,
// since only strategies know what data they need to process a
// payment.
order.processOrder(strategy);

System.out.print("Pay " + order.getTotalCost() + " units or Continue shopping? P/C: ");
String proceed = reader.readLine();
if (proceed.equalsIgnoreCase("P")) {
// Finally, strategy handles the payment.
if (strategy.pay(order.getTotalCost())) {
System.out.println("Payment has been successful.");
} else {
System.out.println("FAIL! Please, check your data.");
}
order.setClosed();
}
}
}
}

OutputDemo.png: 执行结果

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
Please, select a product:
1 - Mother board
2 - CPU
3 - HDD
4 - Memory
1
Count: 2
Do you wish to continue selecting products? Y/N: y
Please, select a product:
1 - Mother board
2 - CPU
3 - HDD
4 - Memory
2
Count: 1
Do you wish to continue selecting products? Y/N: n
Please, select a payment method:
1 - PalPay
2 - Credit Card
1
Enter the user's email: user@example.com
Enter the password: qwerty
Wrong email or password!
Enter user email: amanda@ya.com
Enter password: amanda1985
Data verification has been successful.
Pay 6250 units or Continue shopping? P/C: p
Paying 6250 using PayPal.
Payment has been successful.

在 PHP 中使用模式

复杂度: ★☆☆

流行度: ★★★

使用示例: PHP 代码中经常使用策略模式, 特别是必须在运行时切换算法的情形中。 但是, PHP 从 2009 年开始支持匿名函数, 以其为代表的强大竞争对手挑战着模式的使用。

识别方法: 策略模式可以通过允许嵌套对象完成实际工作的方法以及允许将该对象替换为不同对象的设置器来识别。

概念示例

本例说明了策略设计模式的结构并重点回答了下面的问题:

  • 它由哪些类组成?
  • 这些类扮演了哪些角色?
  • 模式中的各个元素会以何种方式相互关联?

了解该模式的结构后, 你可以更容易地理解下面基于真实世界的 PHP 应用案例。

index.php: 概念示例

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
99
<?php

namespace RefactoringGuru\Strategy\Conceptual;

/**
* The Context defines the interface of interest to clients.
*/
class Context
{
/**
* @var Strategy The Context maintains a reference to one of the Strategy
* objects. The Context does not know the concrete class of a strategy. It
* should work with all strategies via the Strategy interface.
*/
private $strategy;

/**
* Usually, the Context accepts a strategy through the constructor, but also
* provides a setter to change it at runtime.
*/
public function __construct(Strategy $strategy)
{
$this->strategy = $strategy;
}

/**
* Usually, the Context allows replacing a Strategy object at runtime.
*/
public function setStrategy(Strategy $strategy)
{
$this->strategy = $strategy;
}

/**
* The Context delegates some work to the Strategy object instead of
* implementing multiple versions of the algorithm on its own.
*/
public function doSomeBusinessLogic(): void
{
// ...

echo "Context: Sorting data using the strategy (not sure how it'll do it)\n";
$result = $this->strategy->doAlgorithm(["a", "b", "c", "d", "e"]);
echo implode(",", $result) . "\n";

// ...
}
}

/**
* The Strategy interface declares operations common to all supported versions
* of some algorithm.
*
* The Context uses this interface to call the algorithm defined by Concrete
* Strategies.
*/
interface Strategy
{
public function doAlgorithm(array $data): array;
}

/**
* Concrete Strategies implement the algorithm while following the base Strategy
* interface. The interface makes them interchangeable in the Context.
*/
class ConcreteStrategyA implements Strategy
{
public function doAlgorithm(array $data): array
{
sort($data);

return $data;
}
}

class ConcreteStrategyB implements Strategy
{
public function doAlgorithm(array $data): array
{
rsort($data);

return $data;
}
}

/**
* The client code picks a concrete strategy and passes it to the context. The
* client should be aware of the differences between strategies in order to make
* the right choice.
*/
$context = new Context(new ConcreteStrategyA());
echo "Client: Strategy is set to normal sorting.\n";
$context->doSomeBusinessLogic();

echo "\n";

echo "Client: Strategy is set to reverse sorting.\n";
$context->setStrategy(new ConcreteStrategyB());
$context->doSomeBusinessLogic();

Output.txt: 执行结果

1
2
3
4
5
6
7
Client: Strategy is set to normal sorting.
Context: Sorting data using the strategy (not sure how it'll do it)
a,b,c,d,e

Client: Strategy is set to reverse sorting.
Context: Sorting data using the strategy (not sure how it'll do it)
e,d,c,b,a

真实世界示例

在本例中, 策略模式被用于表示电子商务应用中的支付方式。

每种支付方式都可以展示特定的支付表单以收集相应的用户支付信息, 并能将其发送给支付处理公司。 然后, 支付处理公司会将用户重定向回我们的网站, 然后支付方法会验证返回参数并帮助确定订单是否已完成。

index.php: 真实世界示例

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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
<?php

namespace RefactoringGuru\Strategy\RealWorld;

/**
* This is the router and controller of our application. Upon receiving a
* request, this class decides what behavior should be executed. When the app
* receives a payment request, the OrderController class also decides which
* payment method it should use to process the request. Thus, the class acts as
* the Context and the Client at the same time.
*/
class OrderController
{
/**
* Handle POST requests.
*
* @param $url
* @param $data
* @throws \Exception
*/
public function post(string $url, array $data)
{
echo "Controller: POST request to $url with " . json_encode($data) . "\n";

$path = parse_url($url, PHP_URL_PATH);

if (preg_match('#^/orders?$#', $path, $matches)) {
$this->postNewOrder($data);
} else {
echo "Controller: 404 page\n";
}
}

/**
* Handle GET requests.
*
* @param $url
* @throws \Exception
*/
public function get(string $url): void
{
echo "Controller: GET request to $url\n";

$path = parse_url($url, PHP_URL_PATH);
$query = parse_url($url, PHP_URL_QUERY);
parse_str($query, $data);

if (preg_match('#^/orders?$#', $path, $matches)) {
$this->getAllOrders();
} elseif (preg_match('#^/order/([0-9]+?)/payment/([a-z]+?)(/return)?$#', $path, $matches)) {
$order = Order::get($matches[1]);

// The payment method (strategy) is selected according to the value
// passed along with the request.
$paymentMethod = PaymentFactory::getPaymentMethod($matches[2]);

if (!isset($matches[3])) {
$this->getPayment($paymentMethod, $order, $data);
} else {
$this->getPaymentReturn($paymentMethod, $order, $data);
}
} else {
echo "Controller: 404 page\n";
}
}

/**
* POST /order {data}
*/
public function postNewOrder(array $data): void
{
$order = new Order($data);
echo "Controller: Created the order #{$order->id}.\n";
}

/**
* GET /orders
*/
public function getAllOrders(): void
{
echo "Controller: Here's all orders:\n";
foreach (Order::get() as $order) {
echo json_encode($order, JSON_PRETTY_PRINT) . "\n";
}
}

/**
* GET /order/123/payment/XX
*/
public function getPayment(PaymentMethod $method, Order $order, array $data): void
{
// The actual work is delegated to the payment method object.
$form = $method->getPaymentForm($order);
echo "Controller: here's the payment form:\n";
echo $form . "\n";
}

/**
* GET /order/123/payment/XXX/return?key=AJHKSJHJ3423&success=true
*/
public function getPaymentReturn(PaymentMethod $method, Order $order, array $data): void
{
try {
// Another type of work delegated to the payment method.
if ($method->validateReturn($order, $data)) {
echo "Controller: Thanks for your order!\n";
$order->complete();
}
} catch (\Exception $e) {
echo "Controller: got an exception (" . $e->getMessage() . ")\n";
}
}
}

/**
* A simplified representation of the Order class.
*/
class Order
{
/**
* For the sake of simplicity, we'll store all created orders here...
*
* @var array
*/
private static $orders = [];

/**
* ...and access them from here.
*
* @param int $orderId
* @return mixed
*/
public static function get(int $orderId = null)
{
if ($orderId === null) {
return static::$orders;
} else {
return static::$orders[$orderId];
}
}

/**
* The Order constructor assigns the values of the order's fields. To keep
* things simple, there is no validation whatsoever.
*
* @param array $attributes
*/
public function __construct(array $attributes)
{
$this->id = count(static::$orders);
$this->status = "new";
foreach ($attributes as $key => $value) {
$this->{$key} = $value;
}
static::$orders[$this->id] = $this;
}

/**
* The method to call when an order gets paid.
*/
public function complete(): void
{
$this->status = "completed";
echo "Order: #{$this->id} is now {$this->status}.";
}
}

/**
* This class helps to produce a proper strategy object for handling a payment.
*/
class PaymentFactory
{
/**
* Get a payment method by its ID.
*
* @param $id
* @return PaymentMethod
* @throws \Exception
*/
public static function getPaymentMethod(string $id): PaymentMethod
{
switch ($id) {
case "cc":
return new CreditCardPayment();
case "paypal":
return new PayPalPayment();
default:
throw new \Exception("Unknown Payment Method");
}
}
}

/**
* The Strategy interface describes how a client can use various Concrete
* Strategies.
*
* Note that in most examples you can find on the Web, strategies tend to do
* some tiny thing within one method. However, in reality, your strategies can
* be much more robust (by having several methods, for example).
*/
interface PaymentMethod
{
public function getPaymentForm(Order $order): string;

public function validateReturn(Order $order, array $data): bool;
}

/**
* This Concrete Strategy provides a payment form and validates returns for
* credit card payments.
*/
class CreditCardPayment implements PaymentMethod
{
static private $store_secret_key = "swordfish";

public function getPaymentForm(Order $order): string
{
$returnURL = "https://our-website.com/" .
"order/{$order->id}/payment/cc/return";

return <<<FORM
<form action="https://my-credit-card-processor.com/charge" method="POST">
<input type="hidden" id="email" value="{$order->email}">
<input type="hidden" id="total" value="{$order->total}">
<input type="hidden" id="returnURL" value="$returnURL">
<input type="text" id="cardholder-name">
<input type="text" id="credit-card">
<input type="text" id="expiration-date">
<input type="text" id="ccv-number">
<input type="submit" value="Pay">
</form>
FORM;
}

public function validateReturn(Order $order, array $data): bool
{
echo "CreditCardPayment: ...validating... ";

if ($data['key'] != md5($order->id . static::$store_secret_key)) {
throw new \Exception("Payment key is wrong.");
}

if (!isset($data['success']) || !$data['success'] || $data['success'] == 'false') {
throw new \Exception("Payment failed.");
}

// ...

if (floatval($data['total']) < $order->total) {
throw new \Exception("Payment amount is wrong.");
}

echo "Done!\n";

return true;
}
}

/**
* This Concrete Strategy provides a payment form and validates returns for
* PayPal payments.
*/
class PayPalPayment implements PaymentMethod
{
public function getPaymentForm(Order $order): string
{
$returnURL = "https://our-website.com/" .
"order/{$order->id}/payment/paypal/return";

return <<<FORM
<form action="https://paypal.com/payment" method="POST">
<input type="hidden" id="email" value="{$order->email}">
<input type="hidden" id="total" value="{$order->total}">
<input type="hidden" id="returnURL" value="$returnURL">
<input type="submit" value="Pay on PayPal">
</form>
FORM;
}

public function validateReturn(Order $order, array $data): bool
{
echo "PayPalPayment: ...validating... ";

// ...

echo "Done!\n";

return true;
}
}

/**
* The client code.
*/

$controller = new OrderController();

echo "Client: Let's create some orders\n";

$controller->post("/orders", [
"email" => "me@example.com",
"product" => "ABC Cat food (XL)",
"total" => 9.95,
]);

$controller->post("/orders", [
"email" => "me@example.com",
"product" => "XYZ Cat litter (XXL)",
"total" => 19.95,
]);

echo "\nClient: List my orders, please\n";

$controller->get("/orders");

echo "\nClient: I'd like to pay for the second, show me the payment form\n";

$controller->get("/order/1/payment/paypal");

echo "\nClient: ...pushes the Pay button...\n";
echo "\nClient: Oh, I'm redirected to the PayPal.\n";
echo "\nClient: ...pays on the PayPal...\n";
echo "\nClient: Alright, I'm back with you, guys.\n";

$controller->get("/order/1/payment/paypal/return" .
"?key=c55a3964833a4b0fa4469ea94a057152&success=true&total=19.95");

Output.txt: 执行结果

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
Client: Let's create some orders
Controller: POST request to /orders with {"email":"me@example.com","product":"ABC Cat food (XL)","total":9.95}
Controller: Created the order #0.
Controller: POST request to /orders with {"email":"me@example.com","product":"XYZ Cat litter (XXL)","total":19.95}
Controller: Created the order #1.

Client: List my orders, please
Controller: GET request to /orders
Controller: Here's all orders:
{
"id": 0,
"status": "new",
"email": "me@example.com",
"product": "ABC Cat food (XL)",
"total": 9.95
}
{
"id": 1,
"status": "new",
"email": "me@example.com",
"product": "XYZ Cat litter (XXL)",
"total": 19.95
}

Client: I'd like to pay for the second, show me the payment form
Controller: GET request to /order/1/payment/paypal
Controller: here's the payment form:
<form action="https://paypal.com/payment" method="POST">
<input type="hidden" id="email" value="me@example.com">
<input type="hidden" id="total" value="19.95">
<input type="hidden" id="returnURL" value="https://our-website.com/order/1/payment/paypal/return">
<input type="submit" value="Pay on PayPal">
</form>

Client: ...pushes the Pay button...

Client: Oh, I'm redirected to the PayPal.

Client: ...pays on the PayPal...

Client: Alright, I'm back with you, guys.
Controller: GET request to /order/1/payment/paypal/return?key=c55a3964833a4b0fa4469ea94a057152&success=true&total=19.95
PayPalPayment: ...validating... Done!
Controller: Thanks for your order!
Order: #1 is now completed.

在 Python 中使用模式

复杂度: ★☆☆

流行度: ★★★

使用示例: 策略模式在 Python 代码中很常见。 它经常在各种框架中使用, 能在不扩展类的情况下向用户提供改变其行为的方式。

识别方法: 策略模式可以通过允许嵌套对象完成实际工作的方法以及允许将该对象替换为不同对象的设置器来识别。

概念示例

本例说明了策略设计模式的结构并重点回答了下面的问题:

  • 它由哪些类组成?
  • 这些类扮演了哪些角色?
  • 模式中的各个元素会以何种方式相互关联?

main.py: 概念示例

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
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import List


class Context():
"""
The Context defines the interface of interest to clients.
"""

def __init__(self, strategy: Strategy) -> None:
"""
Usually, the Context accepts a strategy through the constructor, but
also provides a setter to change it at runtime.
"""

self._strategy = strategy

@property
def strategy(self) -> Strategy:
"""
The Context maintains a reference to one of the Strategy objects. The
Context does not know the concrete class of a strategy. It should work
with all strategies via the Strategy interface.
"""

return self._strategy

@strategy.setter
def strategy(self, strategy: Strategy) -> None:
"""
Usually, the Context allows replacing a Strategy object at runtime.
"""

self._strategy = strategy

def do_some_business_logic(self) -> None:
"""
The Context delegates some work to the Strategy object instead of
implementing multiple versions of the algorithm on its own.
"""

# ...

print("Context: Sorting data using the strategy (not sure how it'll do it)")
result = self._strategy.do_algorithm(["a", "b", "c", "d", "e"])
print(",".join(result))

# ...


class Strategy(ABC):
"""
The Strategy interface declares operations common to all supported versions
of some algorithm.

The Context uses this interface to call the algorithm defined by Concrete
Strategies.
"""

@abstractmethod
def do_algorithm(self, data: List):
pass


"""
Concrete Strategies implement the algorithm while following the base Strategy
interface. The interface makes them interchangeable in the Context.
"""


class ConcreteStrategyA(Strategy):
def do_algorithm(self, data: List) -> List:
return sorted(data)


class ConcreteStrategyB(Strategy):
def do_algorithm(self, data: List) -> List:
return reversed(sorted(data))


if __name__ == "__main__":
# The client code picks a concrete strategy and passes it to the context.
# The client should be aware of the differences between strategies in order
# to make the right choice.

context = Context(ConcreteStrategyA())
print("Client: Strategy is set to normal sorting.")
context.do_some_business_logic()
print()

print("Client: Strategy is set to reverse sorting.")
context.strategy = ConcreteStrategyB()
context.do_some_business_logic()

Output.txt: 执行结果

1
2
3
4
5
6
7
Client: Strategy is set to normal sorting.
Context: Sorting data using the strategy (not sure how it'll do it)
a,b,c,d,e

Client: Strategy is set to reverse sorting.
Context: Sorting data using the strategy (not sure how it'll do it)
e,d,c,b,a

在 Ruby 中使用模式

复杂度: ★☆☆

流行度: ★★★

使用示例: 策略模式在 Ruby 代码中很常见。 它经常在各种框架中使用, 能在不扩展类的情况下向用户提供改变其行为的方式。

识别方法: 策略模式可以通过允许嵌套对象完成实际工作的方法以及允许将该对象替换为不同对象的设置器来识别。

概念示例

本例说明了策略设计模式的结构并重点回答了下面的问题:

  • 它由哪些类组成?
  • 这些类扮演了哪些角色?
  • 模式中的各个元素会以何种方式相互关联?

main.rb: 概念示例

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
# The Context defines the interface of interest to clients.
class Context
# The Context maintains a reference to one of the Strategy objects. The
# Context does not know the concrete class of a strategy. It should work with
# all strategies via the Strategy interface.
attr_writer :strategy

# Usually, the Context accepts a strategy through the constructor, but also
# provides a setter to change it at runtime.
def initialize(strategy)
@strategy = strategy
end

# Usually, the Context allows replacing a Strategy object at runtime.
def strategy=(strategy)
@strategy = strategy
end

# The Context delegates some work to the Strategy object instead of
# implementing multiple versions of the algorithm on its own.
def do_some_business_logic
# ...

puts 'Context: Sorting data using the strategy (not sure how it\'ll do it)'
result = @strategy.do_algorithm(%w[a b c d e])
print result.join(',')

# ...
end
end

# The Strategy interface declares operations common to all supported versions of
# some algorithm.
#
# The Context uses this interface to call the algorithm defined by Concrete
# Strategies.
class Strategy
# @abstract
#
# @param [Array] data
def do_algorithm(_data)
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end
end

# Concrete Strategies implement the algorithm while following the base Strategy
# interface. The interface makes them interchangeable in the Context.

class ConcreteStrategyA < Strategy
# @param [Array] data
#
# @return [Array]
def do_algorithm(data)
data.sort
end
end

class ConcreteStrategyB < Strategy
# @param [Array] data
#
# @return [Array]
def do_algorithm(data)
data.sort.reverse
end
end

# The client code picks a concrete strategy and passes it to the context. The
# client should be aware of the differences between strategies in order to make
# the right choice.

context = Context.new(ConcreteStrategyA.new)
puts 'Client: Strategy is set to normal sorting.'
context.do_some_business_logic
puts "\n\n"

puts 'Client: Strategy is set to reverse sorting.'
context.strategy = ConcreteStrategyB.new
context.do_some_business_logic

output.txt: 执行结果

1
2
3
4
5
6
7
Client: Strategy is set to normal sorting.
Context: Sorting data using the strategy (not sure how it'll do it)
a,b,c,d,e

Client: Strategy is set to reverse sorting.
Context: Sorting data using the strategy (not sure how it'll do it)
e,d,c,b,a

在 Swift 中使用模式

复杂度: ★☆☆

流行度: ★★★

使用示例: 策略模式在 Swift 代码中很常见。 它经常在各种框架中使用, 能在不扩展类的情况下向用户提供改变其行为的方式。

识别方法: 策略模式可以通过允许嵌套对象完成实际工作的方法以及允许将该对象替换为不同对象的设置器来识别。

概念示例

本例说明了策略设计模式的结构并重点回答了下面的问题:

  • 它由哪些类组成?
  • 这些类扮演了哪些角色?
  • 模式中的各个元素会以何种方式相互关联?

了解该模式的结构后, 你可以更容易地理解下面基于真实世界的 Swift 应用案例。

Example.swift: 概念示例

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
import XCTest

/// The Context defines the interface of interest to clients.
class Context {

/// The Context maintains a reference to one of the Strategy objects. The
/// Context does not know the concrete class of a strategy. It should work
/// with all strategies via the Strategy interface.
private var strategy: Strategy

/// Usually, the Context accepts a strategy through the constructor, but
/// also provides a setter to change it at runtime.
init(strategy: Strategy) {
self.strategy = strategy
}

/// Usually, the Context allows replacing a Strategy object at runtime.
func update(strategy: Strategy) {
self.strategy = strategy
}

/// The Context delegates some work to the Strategy object instead of
/// implementing multiple versions of the algorithm on its own.
func doSomeBusinessLogic() {
print("Context: Sorting data using the strategy (not sure how it'll do it)\n")

let result = strategy.doAlgorithm(["a", "b", "c", "d", "e"])
print(result.joined(separator: ","))
}
}

/// The Strategy interface declares operations common to all supported versions
/// of some algorithm.
///
/// The Context uses this interface to call the algorithm defined by Concrete
/// Strategies.
protocol Strategy {

func doAlgorithm<T: Comparable>(_ data: [T]) -> [T]
}

/// Concrete Strategies implement the algorithm while following the base
/// Strategy interface. The interface makes them interchangeable in the Context.
class ConcreteStrategyA: Strategy {

func doAlgorithm<T: Comparable>(_ data: [T]) -> [T] {
return data.sorted()
}
}

class ConcreteStrategyB: Strategy {

func doAlgorithm<T: Comparable>(_ data: [T]) -> [T] {
return data.sorted(by: >)
}
}

/// Let's see how it all works together.
class StrategyConceptual: XCTestCase {

func test() {

/// The client code picks a concrete strategy and passes it to the
/// context. The client should be aware of the differences between
/// strategies in order to make the right choice.

let context = Context(strategy: ConcreteStrategyA())
print("Client: Strategy is set to normal sorting.\n")
context.doSomeBusinessLogic()

print("\nClient: Strategy is set to reverse sorting.\n")
context.update(strategy: ConcreteStrategyB())
context.doSomeBusinessLogic()
}
}

Output.txt: 执行结果

1
2
3
4
5
6
7
8
9
10
11
Client: Strategy is set to normal sorting.

Context: Sorting data using the strategy (not sure how it'll do it)

a,b,c,d,e

Client: Strategy is set to reverse sorting.

Context: Sorting data using the strategy (not sure how it'll do it)

e,d,c,b,a

真实世界示例

Example.swift: 真实世界示例

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
99
100
101
102
103
104
105
106
107
108
109
import XCTest

class StrategyRealWorld: XCTestCase {

/// This example shows a simple implementation of a list controller that is
/// able to display models from different data sources:
///
/// (MemoryStorage, CoreDataStorage, RealmStorage)

func test() {

let controller = ListController()

let memoryStorage = MemoryStorage<User>()
memoryStorage.add(usersFromNetwork())

clientCode(use: controller, with: memoryStorage)

clientCode(use: controller, with: CoreDataStorage())

clientCode(use: controller, with: RealmStorage())
}

func clientCode(use controller: ListController, with dataSource: DataSource) {

controller.update(dataSource: dataSource)
controller.displayModels()
}

private func usersFromNetwork() -> [User] {
let firstUser = User(id: 1, username: "username1")
let secondUser = User(id: 2, username: "username2")
return [firstUser, secondUser]
}
}

class ListController {

private var dataSource: DataSource?

func update(dataSource: DataSource) {
/// ... resest current states ...
self.dataSource = dataSource
}

func displayModels() {

guard let dataSource = dataSource else { return }
let models = dataSource.loadModels() as [User]

/// Bind models to cells of a list view...
print("\nListController: Displaying models...")
models.forEach({ print($0) })
}
}

protocol DataSource {

func loadModels<T: DomainModel>() -> [T]
}

class MemoryStorage<Model>: DataSource {

private lazy var items = [Model]()

func add(_ items: [Model]) {
self.items.append(contentsOf: items)
}

func loadModels<T: DomainModel>() -> [T] {
guard T.self == User.self else { return [] }
return items as! [T]
}
}

class CoreDataStorage: DataSource {

func loadModels<T: DomainModel>() -> [T] {
guard T.self == User.self else { return [] }

let firstUser = User(id: 3, username: "username3")
let secondUser = User(id: 4, username: "username4")

return [firstUser, secondUser] as! [T]
}
}

class RealmStorage: DataSource {

func loadModels<T: DomainModel>() -> [T] {
guard T.self == User.self else { return [] }

let firstUser = User(id: 5, username: "username5")
let secondUser = User(id: 6, username: "username6")

return [firstUser, secondUser] as! [T]
}
}

protocol DomainModel {

var id: Int { get }
}

struct User: DomainModel {

var id: Int
var username: String
}

Output.txt: 执行结果

1
2
3
4
5
6
7
8
9
10
11
ListController: Displaying models...
User(id: 1, username: "username1")
User(id: 2, username: "username2")

ListController: Displaying models...
User(id: 3, username: "username3")
User(id: 4, username: "username4")

ListController: Displaying models...
User(id: 5, username: "username5")
User(id: 6, username: "username6")

在 TypeScript 中使用模式

复杂度: ★☆☆

流行度: ★★★

使用示例: 策略模式在 TypeScript 代码中很常见。 它经常在各种框架中使用, 能在不扩展类的情况下向用户提供改变其行为的方式。

识别方法: 策略模式可以通过允许嵌套对象完成实际工作的方法以及允许将该对象替换为不同对象的设置器来识别。

概念示例

本例说明了策略设计模式的结构并重点回答了下面的问题:

  • 它由哪些类组成?
  • 这些类扮演了哪些角色?
  • 模式中的各个元素会以何种方式相互关联?

index.ts: 概念示例

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
/**
* The Context defines the interface of interest to clients.
*/
class Context {
/**
* @type {Strategy} The Context maintains a reference to one of the Strategy
* objects. The Context does not know the concrete class of a strategy. It
* should work with all strategies via the Strategy interface.
*/
private strategy: Strategy;

/**
* Usually, the Context accepts a strategy through the constructor, but also
* provides a setter to change it at runtime.
*/
constructor(strategy: Strategy) {
this.strategy = strategy;
}

/**
* Usually, the Context allows replacing a Strategy object at runtime.
*/
public setStrategy(strategy: Strategy) {
this.strategy = strategy;
}

/**
* The Context delegates some work to the Strategy object instead of
* implementing multiple versions of the algorithm on its own.
*/
public doSomeBusinessLogic(): void {
// ...

console.log('Context: Sorting data using the strategy (not sure how it\'ll do it)');
const result = this.strategy.doAlgorithm(['a', 'b', 'c', 'd', 'e']);
console.log(result.join(','));

// ...
}
}

/**
* The Strategy interface declares operations common to all supported versions
* of some algorithm.
*
* The Context uses this interface to call the algorithm defined by Concrete
* Strategies.
*/
interface Strategy {
doAlgorithm(data: string[]): string[];
}

/**
* Concrete Strategies implement the algorithm while following the base Strategy
* interface. The interface makes them interchangeable in the Context.
*/
class ConcreteStrategyA implements Strategy {
public doAlgorithm(data: string[]): string[] {
return data.sort();
}
}

class ConcreteStrategyB implements Strategy {
public doAlgorithm(data: string[]): string[] {
return data.reverse();
}
}

/**
* The client code picks a concrete strategy and passes it to the context. The
* client should be aware of the differences between strategies in order to make
* the right choice.
*/
const context = new Context(new ConcreteStrategyA());
console.log('Client: Strategy is set to normal sorting.');
context.doSomeBusinessLogic();

console.log('');

console.log('Client: Strategy is set to reverse sorting.');
context.setStrategy(new ConcreteStrategyB());
context.doSomeBusinessLogic();

Output.txt: 执行结果

1
2
3
4
5
6
7
Client: Strategy is set to normal sorting.
Context: Sorting data using the strategy (not sure how it'll do it)
a,b,c,d,e

Client: Strategy is set to reverse sorting.
Context: Sorting data using the strategy (not sure how it'll do it)
e,d,c,b,a

概念示例

思考一下构建内存缓存的情形。 由于处在内存中, 故其大小会存在限制。 在达到其上限后, 一些条目就必须被移除以留出空间。 此类操作可通过多种算法进行实现。 一些流行的算法有:

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

问题在于如何将我们的缓存类与这些算法解耦, 以便在运行时更改算法。 此外, 在添加新算法时, 缓存类不应改变。

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

现在, 我们的主要缓存类将嵌入至 eviction­Algo接口中。 缓存类会将全部类型的移除算法委派给 eviction­Algo接口, 而不是自行实现。 鉴于 eviction­Algo是一个接口, 我们可在运行时将算法更改为 LRU、 FIFO 或者 LFU, 而不需要对缓存类做出任何更改。

evictionAlgo.go: 策略接口

1
2
3
4
5
package main

type evictionAlgo interface {
evict(c *cache)
}

fifo.go: 具体策略

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

type fifo struct {
}

func (l *fifo) evict(c *cache) {
fmt.Println("Evicting by fifo strtegy")
}

lru.go: 具体策略

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

type lru struct {
}

func (l *lru) evict(c *cache) {
fmt.Println("Evicting by lru strtegy")
}

lfu.go: 具体策略

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

type lfu struct {
}

func (l *lfu) evict(c *cache) {
fmt.Println("Evicting by lfu strtegy")
}

cache.go: 背景

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
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)
c.capacity--
}

main.go: 客户端代码

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

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

cache.add("a", "1")
cache.add("b", "2")

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

lru := &lru{}
cache.setEvictionAlgo(lru)

cache.add("d", "4")

fifo := &fifo{}
cache.setEvictionAlgo(fifo)

cache.add("e", "5")

}

output.txt: 执行结果

1
2
3
Evicting by lfu strtegy
Evicting by lru strtegy
Evicting by fifo strtegy

根据: Golang By Example