mohuneko’s blog

かんばる駆け出しエンジニアのブログです

【Java】Strategyパターン【デザインパターン】

駆け出しエンジニアがデザインパターンをもくもく勉強します

 こんな本で勉強しています🌟

目次

Strategyパターンについて

Strategyパターンとは

  • 問題を解くためのアルゴリズムをごっそり交換できるパターンです
  • アルゴリズム (方法・戦略・方策) を簡単に変更し、同じ問題を別の方法で解くことができます

サンプルプログラム

  • Strategyパターンを使って、じゃんけんする例を取り上げます。"勝ったら次も同じ手を出す戦略"と、"一回前の手から次の手を確率的に計算する戦略"があります
  • 各クラスの役割は以下のようになっています
名前 役割
Hand 手を表すクラス
Strategy 戦略を表すインターフェース
WinningStrategy (ConcreteStrategy) 勝ったら次も同じ手を出す戦略を表すクラス
ProbStrategy (ConcreteStrategy) 一回前の手から次の手を確率的に計算する戦略を表すクラス
Player (Context) じゃんけんを行うプレイヤー

f:id:mohuNeko:20201229151617p:plain

Handクラス

  • グー・チョキ・パーを0・1・2のintで表現します
  • Handクラスではインスタンスは3つしか作られません (Singletonパターンの一種)
  • (this.handvalue + 1) % 3 == h.handvalueでは、じゃんけんに勝ったかどうかを判断しています
    • thisの値に1を加えたものがhの手の値(thisがグーならhがチョキ、thisがチョキならhはパー)になっていたら、trueです
    • %3で剰余を取っているのは、thisが2(パー)の時に、1を加えて0 (グー)になるようにするためです
public class Hand {
    public static final int HANDVALUE_GUU = 0;  
    public static final int HANDVALUE_CHO = 1;  
    public static final int HANDVALUE_PAA = 2;  
    public static final Hand[] hand = {         // じゃんけんの手を表す3つのインスタンス
        new Hand(HANDVALUE_GUU),
        new Hand(HANDVALUE_CHO),
        new Hand(HANDVALUE_PAA),
    };
    private static final String[] name = {      // じゃんけんの手の文字列表現
        "グー", "チョキ", "パー",
    };
    private int handvalue;                      // じゃんけんの手の値
    private Hand(int handvalue) {
        this.handvalue = handvalue;
    }
    public static Hand getHand(int handvalue) { // 値からインスタンスを得る
        return hand[handvalue];
    }
    public boolean isStrongerThan(Hand h) {     // thisがhより強いときtrue
        return fight(h) == 1;
    }
    public boolean isWeakerThan(Hand h) {       // thisがhより弱いときtrue
        return fight(h) == -1;
    }
    private int fight(Hand h) {                 // 引き分けは0, thisの勝ちなら1, hの勝ちなら-1
        if (this == h) {
            return 0;
        } else if ((this.handvalue + 1) % 3 == h.handvalue) {
            return 1;
        } else {
            return -1;
        }
    }
    public String toString() {                 
        return name[handvalue];
    }
}

Strategyインターフェース

  • じゃんけんの戦略のための抽象メソッドを集めたインターフェースです
    • nextHandは次にだす手を得るメソッドで、これが呼ばれたら、Strategyインターフェース実装クラスが次の一手を決めます
  • studyは"前回の手が勝ったかどうか"を判定するメソッドで、勝ったらstudy(true)、負けたらstudy(false)として呼び出します
  • こうすることで、Strategy実装クラスが自分の内部状態を変更し、次回以降のnextHandメソッドの戻り値を決定する材料にします
public interface Strategy {
    public abstract Hand nextHand();
    public abstract void study(boolean win);
}

WinningStrategyクラス

  • Strategyインターフェース実装クラスの一つです
    • nextHandとstudyメソッドを実装します
  • ここでの戦略は"前回勝ったら、次回も同じ手を出す"、"前回負けたら、次の手はランダムに出す"という方針です
import java.util.Random;

public class WinningStrategy implements Strategy {
    private Random random;
    private boolean won = false; //前回の結果を保持する
    private Hand prevHand; //前回の手を保持する

    public WinningStrategy(int seed) {
        random = new Random(seed); //乱数を発生させる
    }
    public Hand nextHand() {
        if (!won) { //wonがtrueの場合にfalse、wonがfalseの場合にtrue
            prevHand = Hand.getHand(random.nextInt(3)); //負けたらランダム
        }
        return prevHand; //勝ったら同じ手
    }
    public void study(boolean win) {
        won = win;
    }
}

ProbStrategyクラス

  • Strategyインターフェース実装クラスの一つです
  • 次の手は乱数で決定しますが、過去の勝ち負けの履歴を使って確率を変更します
  • historyフィールドで過去の勝敗を反映した表を表します(history[前回の手][今回の手])
    • history[0][0]の場合、グー、グーと出した時の過去の勝ち数です
    • 前回グーの場合、history[0][0],history[0][1],history[0][2]の値の和を計算し、0からその数までの乱数を計算して、それを元に次の手を決めます
  • studyメソッドは、nextHandメソッドで返した手の勝敗をもとにhistoryフィールドの内容を変更します
import java.util.Random;

public class ProbStrategy implements Strategy {
    private Random random;
    private int prevHandValue = 0;
    private int currentHandValue = 0;
    private int[][] history = {
        { 1, 1, 1, },
        { 1, 1, 1, },
        { 1, 1, 1, },
    };
    public ProbStrategy(int seed) {
        random = new Random(seed);
    }
    public Hand nextHand() {
        int bet = random.nextInt(getSum(currentHandValue));
        int handvalue = 0;
        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(handvalue);
    }
    private int getSum(int hv) {
        int sum = 0;
        for (int i = 0; i < 3; i++) {
            sum += history[hv][i];
        }
        return sum;
    }
    public void study(boolean win) {
        if (win) {
            history[prevHandValue][currentHandValue]++;
        } else {
            history[prevHandValue][(currentHandValue + 1) % 3]++;
            history[prevHandValue][(currentHandValue + 2) % 3]++;
        }
    }
}

Playerクラス

  • じゃんけんを行う人を表現したクラスです
  • 名前と戦略を与えられてインスタンスを作成します
  • nextHandメソッドは次の手を得ますが、実際に決定するのは戦略です
    • strategy.nextHandの戻り値が、そのままPlayer のnextHandメソッドの戻り値になります
    • つまり、nextHandメソッドは処理をStrategyに委譲していることになります
  • 勝敗結果を保持して次の戦略に活かすために、strategyフィールドを通じてstudyメソッドを呼びます
    • studyメソッドを使って戦略の内部状態を変化させます
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++;
    }
    public String toString() {
        return "[" + name + ":" + gamecount + " games, " + wincount + " win, " + losecount + " lose" + "]";
    }
}

Mainクラスで動作確認

  • 名前と戦略を渡して10000回じゃんけんを行います
public class Main {
    public static void main(String[] args) {
        if (args.length != 2) {
            System.out.println("Usage: java Main randomseed1 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 player1 = new Player("Taro", new WinningStrategy(seed1));
        Player player2 = new Player("Hana", new ProbStrategy(seed2));
        for (int i = 0; i < 10000; i++) {
            Hand nextHand1 = player1.nextHand();
            Hand nextHand2 = player2.nextHand();
            if (nextHand1.isStrongerThan(nextHand2)) {
                System.out.println("Winner:" + player1);
                player1.win();
                player2.lose();
            } else if (nextHand2.isStrongerThan(nextHand1)) {
                System.out.println("Winner:" + player2);
                player1.lose();
                player2.win();
            } else {
                System.out.println("Even...");
                player1.even();
                player2.even();
            }
        }
        System.out.println("Total result:");
        System.out.println(player1.toString());
        System.out.println(player2.toString());
    }
}

コンパイルして実行します

  • javac -encoding EUC-JP Main.java
  • java Main 314 15 //乱数を引数に
Even...
Winner:[Hana:1 games, 0 win, 0 lose]
Winner:[Taro:2 games, 0 win, 1 lose]
Even...
Winner:[Hana:4 games, 1 win, 1 lose]
Winner:[Taro:5 games, 1 win, 2 lose]
Even...
Even...
Winner:[Taro:8 games, 2 win, 2 lose]
Winner:[Taro:9 games, 3 win, 2 lose]
Winner:[Taro:10 games, 4 win, 2 lose]
Even...

//中略

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]

Strategyパターンのメリット

  • Strategyパターンでは、アルゴリズムの部分をほかの部分と意識的に分離します
  • アルゴリズムとのインターフェースの部分だけを規定し、委譲によってアルゴリズムを利用します
  • こうすることで、アルゴリズムを改良してもっと高速にしたい場合、Strategyインターフェースを変更せず、アルゴリズムをだけを追加、修正すればいいのです
    • 委譲というゆるやかな結びつきを使っているのため、アルゴリズムを用意に切り替えることができます。
    • ゲームプログラム等では、ユーザーの選択に合わせて難易度を切り替えるなど
  • また、プログラム実行中にConcreteStrategyを変更することもできます
    • メモリの少ない環境や、多い環境で戦略を変更するなど

今日のポイント

  • Strategyパターンは、問題を解くためのアルゴリズムをごっそり交換できるパターンです
    • アルゴリズム (方法・戦略・方策) を簡単に変更し、同じ問題を別の方法で解くことができます
  • Strategyパターンでは、アルゴリズムの部分を他と意識的に分離します
  • アルゴリズムとのインターフェースの部分だけを規定し、委譲によってアルゴリズムを利用します
  • こうすることで、アルゴリズムを改良したい場合、Strategyインターフェースを変更せず、アルゴリズムをだけを追加、修正すればいいことになります

 本日もお疲れ様です😊