10.1 策略模式
Strategy 的意思是 “策略”,指的是与敌军对垒时行军作战的方法。在编程中,我们可以将其理解为 “算法”。
无论什么程序,其目的都是解决问题。而为了解决问题,我们需要编写特定的算法。使用 Stratety 模式可以整体地替换算法的实现部分。能够整体地替换算法,能让我们轻松地以不同的算法去解决同一个问题,这种模式就是 Stratety 模式。
10.2 示例程序
示例程序的功能是让电脑玩 “猜拳” 游戏。我们考虑了两种猜拳策略。
- 第一种策略是 “如果这局猜拳获胜,那么下一局也出一样的手势”,这是一种有些笨的策略
- 另外一种策略是 “根据上一局的手势从概率上计算出下一局的手势”。
各类的功能如下:
类名 | 功能 |
---|---|
Hand | 表示猜拳游戏中的 “手势” 类 |
Strategy | 表示猜拳游戏中策略的类 |
Player | 表示进行游戏猜拳的选手 |
WinningStrategy | 第一种策略类 |
ProbStrategy | 第二种策略类 |
|| Hand 类
Hand 类表示猜拳游戏中 “手势” 的类。在其内部,用 int 表示所出的手势, 0 > 石头、1 > 剪刀、2 > 布。
Hand 类的实例可以通过使用类方法 getHand 来获取。只要将表示手势的值作为参数传递给 getHand 方法,它就会将手势的值所对应的 Hand 类的实例返回给我们,这是一中 Singleton 模式。
isStrongerThan 方法和 isWeakerThan 方法用于判断猜拳结果。类中,实际负责判断猜拳结果的是 fight 方法,其判断依据是手势的值。其中 (this.handValue + 1) % 3 == h.handValue 的意思是,手势值 + 1 后与 3 的余数是否与需要比较的值相同(如果当前是石头,需要比较的值是剪刀,石头 + 1 后与 3 的余数是剪刀,剪刀 == 剪刀成立,但石头是赢剪刀的,所以这里返回 1 胜)。
虽然 Hand 类会被其他类使用,但其并非 Strategy 模式中的角色。
/**
* 猜拳游戏中的手势类.
*/
public class Hand {
private static int HAND_VALUE_GUU = 0; // 石头
private static int HAND_VALUE_CHO = 1; // 剪刀
private static int HAND_VALUE_PAA = 2; // 布
public static Hand[] hand = {
new Hand(HAND_VALUE_GUU),
new Hand(HAND_VALUE_CHO),
new Hand(HAND_VALUE_PAA)
};
private static final String[] name = {
"石头", "剪刀", "布"
};
private int handValue; // 猜拳手势值
private Hand(int value) {
this.handValue = value;
}
public static Hand getHand(int handValue) {
return hand[handValue];
}
public boolean isStrongerThan(Hand h) { // 是否获胜
return fight(h) == 1;
}
public boolean isWeakerThan(Hand h) { // 是否输了
return fight(h) == -1;
}
private int fight(Hand hand) { // 0:平,1:胜,-1 负
if (this == hand) {
return 0;
}
// 石头 > 剪刀, 剪刀 > 布, 布 > 石头
// 如果当前是剪刀,对方出的布,则 + 1 除3余数与布相当,返回1,胜
if ((this.handValue + 1) % 3 == hand.handValue) {
return 1;
} else {
return -1;
}
}
@Override
public String toString() {
return name[handValue];
}
}
|| Strategy 接口
Strategy 接口定义了猜拳策略的抽象方法的接口。
nextHand 方法的作用就是 “获取下一局要出的手势”。调用了该方法后,实现了 Strategy 接口的实现类会想出下一局出什么手势。
study 方法的作用是学习 “上一局的手势是否获胜了”。如果在上一局中调用 nextHand 方法获胜了,接着就会调用 study(true),反之,则传入 false。
/**
* 定义了猜拳策略的抽象方法接口.
*/
public interface Strategy {
// 获取下一个手势的方法
Hand nextHand();
// 学习上一个手势是否获胜的方法,便于计算策略
void study(boolean isWin);
}
|| WinningStrategy 类
具体的猜拳策略实现类:如果上一局手势获胜了,则下一局出相同的手势,否则,则随机出一个手势。
/**
* 猜拳策略:如果上一局获胜,则继续出一样的手势,如果失败,则随机出手势.
*/
public class WinningStrategy implements Strategy{
private Random rand;
private boolean won = false;
private Hand prevHand;
public WinningStrategy(int seed) {
rand = new Random(seed);
}
@Override
public Hand nextHand() {
if (!won) {
prevHand = Hand.getHand(rand.nextInt(3));
}
return prevHand;
}
@Override
public void study(boolean isWin) {
won = isWin;
}
}
|| ProbStrategy 类 [权重分配]
ProbStrategy 类是另外一个具体的策略,虽然与 WinningStrategy 类一样,也是随机出手势,但是每种手势出现的概率会根据以前的猜拳结果而改变。
history 字段是一个表,被用于根据过去的胜负来进行概率计算。它是一个二维数组,每个数组下标的意思如下。
history[上一局出的手势][这一局出的手势]
这个表达式的值越大,表示过去的胜利越高。
例如,假如我们上一局出的是石头
- history[0][0] 两局分别出石头、石头时胜了的次数
- history[0][1] 两局分别出石头、剪刀时胜了的次数
- history[0][2] 两局分别出石头、布 时胜了的次数
那么我们就可以根据这3个表达式的值从概率上计算出下一局出什么。简而言之,就是先计算 3 个表达式的值的和,然后再从 0 与这个和之间取一个随机数,并据此决定下一局应该出什么。例如:
- history[0][0] 是 3
- history[0][1] 是 5
- history[0][1] 是 7
那么,下一局出 石头、剪刀、布的比率就是 3:5:7 来决定。然后在 0 至 15 之间取一个随机数,根据所在区间来选择对应的手势。
/**
* 策略:根据手势赢面的权重,生成不同的手势.
* 手势出现的概率歌剧以前猜拳的概率结果而改变
*/
public class ProbStrategy implements Strategy{
private Random rand;
private int prevHandValue = 0;
private int currentHandValue = 0;
// history[0][0] - 两次分别出 石头 石头 的获胜次数
// history[0][1] - 两次分别出 石头 剪刀 的获胜次数
// history[0][2] - 两次分别出 石头 布 的获胜次数
// 同理还有 剪刀 、 布 的情况
private int[][] history = {
{1,1,1},
{1,1,1},
{1,1,1}
};
public ProbStrategy(int seed) {
this.rand = new Random(seed);
}
@Override
public Hand nextHand() {
// 根据权重范围,挑选随机值所在的区间,获取手势
int bet = rand.nextInt(getSum(currentHandValue));
int handValue;
if (bet < history[currentHandValue][0]) {
handValue = 0;
} else if (bet < (history[currentHandValue][0] + history[currentHandValue][1])) {
handValue = 1;
} else {
handValue = 2;
}
prevHandValue = currentHandValue;
currentHandValue = handValue;
return Hand.getHand(currentHandValue);
}
// 计算当前猜拳情况下各可能的获胜总和
// 比如当前是1,则计算 1 0 , 1 1 , 1 2 的总获胜次数
private int getSum(int hv) {
int sum = 0;
for (int i = 0; i < 3; i++) {
sum += history[hv][i];
}
return sum;
}
@Override
public void study(boolean isWin) {
if (isWin) {
history[prevHandValue][currentHandValue]++; // 获胜,则当前记录 + 1,否则另外两种都加1
} else {
history[prevHandValue][(currentHandValue + 1) % 3]++;
history[prevHandValue][(currentHandValue + 2) % 3]++;
}
}
}
|| Player 类
Player 类是表示进行猜拳游戏的选手的类。生成时,需要向其传递 “姓名” 和 “策略”。包含了胜、负和平局的处理方法。
/**
* 猜拳参赛选手.
*/
public class Player {
private String name;
private Strategy strategy;
private int winCount;
private int loseCount;
private int gameCount;
public Player(String name, Strategy strategy) {
this.name = name;
this.strategy = strategy; // 策略赋予
}
public Hand nextHand() {
return strategy.nextHand();
}
public void win() {
strategy.study(true);
winCount++;
gameCount++;
}
public void lose() {
strategy.study(false);
loseCount++;
gameCount++;
}
public void even() {
gameCount++;
}
@Override
public String toString() {
return "[" + name + ":" + gameCount + " games," + winCount + " win," + loseCount + " lose" + "]";
}
}
|| Main 类
负责使用以上类让电脑进行猜拳游戏。进行比赛,然后显示比赛结果。
public class Main {
public static void main(String[] args) {
if (args.length != 2) {
System.out.println("Usage: java Main randomSeed randomSeed2");
System.out.println("Example: java Main 314 15");
System.exit(0);
}
int seed1 = Integer.parseInt(args[0]);
int seed2 = Integer.parseInt(args[1]);
Player taro = new Player("Taro", new WinningStrategy(seed1));
Player hana = new Player("Hana", new ProbStrategy(seed2));
for (int i = 0; i < 10000; i++) {
Hand taroHand = taro.nextHand();
Hand hanaHand = hana.nextHand();
if (taroHand.isStrongerThan(hanaHand)) {
System.out.println("Winner:" + taro);
taro.win();
hana.lose();
} else if (taroHand.isWeakerThan(hanaHand)) {
System.out.println("Winner:" + hana);
taro.lose();
hana.win();
} else {
System.out.println("Even...");
taro.even();
hana.even();
}
}
System.out.println("Total result:");
System.out.println(taro);
System.out.println(hana);
}
}
运行结果:
...
Winner:[Hana:9993 games,3488 win,3165 lose]
Winner:[Taro:9994 games,3165 win,3489 lose]
Winner:[Taro:9995 games,3166 win,3489 lose]
Winner:[Hana:9996 games,3489 win,3167 lose]
Even...
Even...
Even...
Total result:
[Taro:10000 games,3167 win,3490 lose]
[Hana:10000 games,3490 win,3167 lose]
10.3 Strategy 模式中的登场角色
◆ Strategy (策略)
Strategy 角色负责实现策略所必须的接口(API)。在示例程序中,由 Strategy 接口扮演此角色。
◆ ConcreteStrategy (具体的策略)
ConcreteStrategy 角色负责实现 Strategy 角色的接口,即负责实现具体的策略(战略、方法、算法)。在示例程序中由 WinningStrategy 和 ProbStrategy 类扮演此角色。
◆ Context (上下文)
负责使用 Strategy 角色。Context 角色保存了 ConcreteStrategy 角色的实例,并使用 ConcreteStrategy 角色去实现需求(总之还是需要调用 Strategy 角色的接口)。在示例程序中,由 Player 类扮演此角色。
10.4 拓展思路的要点
|| 为什么需要特意编写 Strategy 角色
通常在编程时算法会被写在具体的方法中。Strategy 模式却特意的将算法与其他部分分离开来,只是定义了与算法相关的接口,然后在程序中以委托的方式来使用算法。
这样看起来好像程序变复杂了,其实不然。例如,我们想要通过改善算法来提高算法的处理速度时,如果使用了 Strategy 模式,就不必修改 Strategy 角色的接口了。仅仅修改 ConcreteStrategy 角色即可。而且,使用委托这种弱关系可以很方便地整体替换算法。
比如,使用 Strategy 模式编写象棋程序时,可以方便地根据棋手的选择切换 AI 例程的水平。
|| 程序运行时也可以切换策略
如果使用 Strategy 模式,在程序运行时也可以切换 ConcreteStrategy 角色。例如,在内存容量少的运行环境中使用速度慢但节约内存的策略,内存容量多的运行环境中则可以使用 速度快但耗内存的策略。
还可以使用某种算法来 “验算” 另外一种算法。例如,某一个 高速但可能存在 Bug 的算法 和 低速但计算准确的算法,然后让后者去验算前者的计算结果。