策略模式

意图

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

问题

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

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

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

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

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

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

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

解决方案

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

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

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

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

路线规划策略

路线规划策略

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

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

伪代码

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

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

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

  • java.util.Comparator#compare() 的调用来自 Collections#sort().

  • javax.servlet.http.HttpServlet service­()方法还有所有接受 Http­Servlet­Request Http­Servlet­Response对象作为参数的 do­XXX()方法

  • javax.servlet.Filter#doFilter()

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

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

在本例中策略模式被用于在电子商务应用中实现各种支付方法客户选中希望购买的商品后需要选择一种支付方式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/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.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
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.

策略模式适合应用场景

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

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

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

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

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

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

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

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

实现方式

  1. 从上下文类中找出修改频率较高的算法也可能是用于在运行时选择某个算法变体的复杂条件运算符)。

  2. 声明该算法所有变体的通用策略接口

  3. 将算法逐一抽取到各自的类中它们都必须实现策略接口

  4. 在上下文类中添加一个成员变量用于保存对于策略对象的引用然后提供设置器以修改该成员变量上下文仅可通过策略接口同策略对象进行交互如有需要还可定义一个接口来让策略访问其数据

  5. 客户端必须将上下文类与相应策略进行关联使上下文可以预期的方式完成其主要工作

策略模式优缺点

  • 你可以在运行时切换对象内的算法

  • 你可以将算法的实现和使用算法的代码隔离开来

  • 你可以使用组合来代替继承

  • 开闭原则你无需对上下文进行修改就能够引入新的策略

  • 如果你的算法极少发生改变那么没有任何理由引入新的类和接口使用该模式只会让程序过于复杂

  • 客户端必须知晓策略间的不同——它需要选择合适的策略

  • 许多现代编程语言支持函数类型功能允许你在一组匿名函数中实现不同版本的算法这样你使用这些函数的方式就和使用策略对象时完全相同无需借助额外的类和接口来保持代码简洁

与其他模式的关系

  • 桥接模式状态模式策略模式在某种程度上包括适配器模式模式的接口非常相似实际上它们都基于组合模式——即将工作委派给其他对象不过也各自解决了不同的问题模式并不只是以特定方式组织代码的配方你还可以使用它们来和其他开发者讨论模式所解决的问题

  • 命令模式策略看上去很像因为两者都能通过某些行为来参数化对象但是它们的意图有非常大的不同

    • 你可以使用*命令*来将任何操作转换为对象操作的参数将成为对象的成员变量你可以通过转换来延迟操作的执行将操作放入队列保存历史命令或者向远程服务发送命令等

    • 另一方面*策略*通常可用于描述完成某件事的不同方式让你能够在同一个上下文类中切换算法

  • 装饰模式可让你更改对象的外表策略则让你能够改变其本质

  • 模板方法模式基于继承机制它允许你通过扩展子类中的部分内容来改变部分算法策略基于组合机制你可以通过对相应行为提供不同的策略来改变对象的部分行为*模板方法在类层次上运作因此它是静态的策略*在对象层次上运作因此允许在运行时切换行为

  • 状态可被视为策略的扩展两者都基于组合机制它们都通过将部分工作委派给帮手对象来改变其在不同情景下的行为*策略使得这些对象相互之间完全独立它们不知道其他对象的存在状态*模式没有限制具体状态之间的依赖且允许它们自行改变在不同情景下的状态