Strategy (策略)模式

10.1 策略模式

  Strategy 的意思是 “策略”,指的是与敌军对垒时行军作战的方法。在编程中,我们可以将其理解为 “算法”。
  无论什么程序,其目的都是解决问题。而为了解决问题,我们需要编写特定的算法。使用 Stratety 模式可以整体地替换算法的实现部分。能够整体地替换算法,能让我们轻松地以不同的算法去解决同一个问题,这种模式就是 Stratety 模式。

10.2 示例程序

  示例程序的功能是让电脑玩 “猜拳” 游戏。我们考虑了两种猜拳策略。

  • 第一种策略是 “如果这局猜拳获胜,那么下一局也出一样的手势”,这是一种有些笨的策略
  • 另外一种策略是 “根据上一局的手势从概率上计算出下一局的手势”。

各类的功能如下:

类名功能
Hand表示猜拳游戏中的 “手势” 类
Strategy表示猜拳游戏中策略的类
Player表示进行游戏猜拳的选手
WinningStrategy第一种策略类
ProbStrategy第二种策略类

Strategy 示例程序类图

Strategy 示例程序类图

|| 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 类扮演此角色。
Strategy 模式UML图

Strategy 模式UML图
10.4 拓展思路的要点
|| 为什么需要特意编写 Strategy 角色

  通常在编程时算法会被写在具体的方法中。Strategy 模式却特意的将算法与其他部分分离开来,只是定义了与算法相关的接口,然后在程序中以委托的方式来使用算法。
  这样看起来好像程序变复杂了,其实不然。例如,我们想要通过改善算法来提高算法的处理速度时,如果使用了 Strategy 模式,就不必修改 Strategy 角色的接口了。仅仅修改 ConcreteStrategy 角色即可。而且,使用委托这种弱关系可以很方便地整体替换算法
  比如,使用 Strategy 模式编写象棋程序时,可以方便地根据棋手的选择切换 AI 例程的水平。

|| 程序运行时也可以切换策略

  如果使用 Strategy 模式,在程序运行时也可以切换 ConcreteStrategy 角色。例如,在内存容量少的运行环境中使用速度慢但节约内存的策略,内存容量多的运行环境中则可以使用 速度快但耗内存的策略。
  还可以使用某种算法来 “验算” 另外一种算法。例如,某一个 高速但可能存在 Bug 的算法 和 低速但计算准确的算法,然后让后者去验算前者的计算结果。
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值