【Java】Strategyパターン【デザインパターン】
目次
Strategyパターンについて
Strategyパターンとは
サンプルプログラム
- Strategyパターンを使って、じゃんけんする例を取り上げます。"勝ったら次も同じ手を出す戦略"と、"一回前の手から次の手を確率的に計算する戦略"があります
- 各クラスの役割は以下のようになっています
名前 | 役割 |
---|---|
Hand | 手を表すクラス |
Strategy | 戦略を表すインターフェース |
WinningStrategy (ConcreteStrategy) | 勝ったら次も同じ手を出す戦略を表すクラス |
ProbStrategy (ConcreteStrategy) | 一回前の手から次の手を確率的に計算する戦略を表すクラス |
Player (Context) | じゃんけんを行うプレイヤー |
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[前回の手][今回の手])
- 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インターフェースを変更せず、アルゴリズムをだけを追加、修正すればいいことになります
本日もお疲れ様です😊