#はじめに
本エントリーは某社内で実施するデザインパターン勉強会向けの資料となります。
本エントリーで書籍「Java言語で学ぶデザインパターン入門」をベースに学習を進めますが、サンプルコードはC#に置き換えて解説します。
第1回:Iteratorパターン
第2回:Adapterパターン
第3回:Template Methodパターン
第4回:Factory Methodパターン
第5回:Singletonパターン
第6回:Prototypeパターン
第7回:Builderパターン
第8回:Abstract Factoryパターン
第9回:Bridgeパターン
#Strategyパターンとは
Strategyとは直訳すると、「戦略」という意味になります。プログラミングエリアでは、「アルゴリズム」という意味になります。
プログラミングで何かを実現するとき、それを実現するための特定の解法(アルゴリズム)があります。またプログラミングにおいて実現するためのアルゴリズムは一つである事とは限らず。時と場合によっては別のアルゴリズムを使う場合があります。
現実に例えれば、ある日お腹がすいてラーメンを食べたいと思ったとき、ラーメンを食べるために行う行動(Strategy)はいくつか考えられます。自分で作る(カップラーメン、パック麺、冷凍生麺)、店に行く、出前を取る、家の誰かに作ってもらう等です。
プログラミングで例えればデータをソートするアルゴリズムについて考えてみましょう。ソートするプログラムを作る時に以下のようなアルゴリズムが考えられ、それぞれ状況により使い分ける必要があります。。
・バブルソート
・シェルソート
・マージソート
・ヒープソート
・クイックソート
・バケットソート
※バブルソートは計算量が多いですが、比較的シンプルなコードになります。(データ量が少なく簡易的なソートを実施したい場合に使えます。)
※ヒープソートは複雑なコードになりますが、計算量、メモリ使用量ともに小さく抑えることができます。(大量のデータをソートする)
※メモリを消費してでも速さを優先し、ソートするデータが一意である場合はバケットソートが適しているかもしれません。
このような時、複数の人が何も考えずに各々が自分の作成したプログラムで実装してしまうと、他の開発者がソートを使用したいときの再利用性が悪いだけでなく、作成したアルゴリズムのメンテナンスも煩雑になってしまします。
Strategyパターンは最終的に目指すゴールが同じものの、ゴールへと向かう方法(アルゴリズム)をプログラムから切り離して設計し、管理することで、アルゴリズムのメンテナンスを容易にしリサイクル性を向上させることができる上に、プログラム実行時に動的にアルゴリズムを切り替えるようなこともできます。
Strategyパターンでのクラスは以下のようになります。
名 前 | 解 説 |
---|---|
Strategy | 戦略を利用するためのインターフェース(API)を定めます。 |
Concrete Strategy | Strategyによって定められたインターフェースに則り実装します。 |
Context | Concrete StrategyをStrategyを介して利用するクラスです。 |
次に実際の実装例を見てみましょう。
#実装例
ここでは、例として「じゃんけん」プログラムを使用します。
2つの違うStrategyを持ったプレーヤーを戦わせどちらの勝敗が多いかを表示するコンソールプログラムです。
#サンプルクラス一覧
名 前 | 役 割 | 解 説 |
---|---|---|
Hand | じゃんけんの「手」を表すクラス | |
Strategy | Strategy | じゃんけんの「戦略」を表すインタフェース |
WonStrategy | Concrete Strategy | 勝ったら次も同じ手を出す戦略 |
ProbStrategy | Concrete Strategy | 一回前の手から次の手を確立的に計算する戦略 |
Player | Context | じゃんけんのプレイヤーを表すクラス |
StrategyPattern | Main | 動作テスト用のクラス |
#ソースコード
ソースコードの詳細な説明は以下の本を参照してください。
「Java言語で学ぶデザインパターン入門」
#Handクラス
Handクラスは、じゃんけんの「手」を表すクラスです。
手の強さの比較や判定もこのクラスで行います。
using System;
public class Hand
{
public const int HANDVALUE_STONE = 0; //int value of stone
public const int HANDVALUE_SCISSORS = 1; // int value of scissors
public const int HANDVALUE_PAPER = 2; // int value of paper
public static Hand[] hand = new Hand[] { new Hand(HANDVALUE_STONE), new Hand(HANDVALUE_SCISSORS), new Hand(HANDVALUE_PAPER) }; //three hand moves objects
private static readonly String[] name = new String[] { "STONE", "SCISSORS", "PAPER" }; //hand string expression
private int handvalue; //this hand move
private Hand(int handvalue)
{
this.handvalue = handvalue;
}
public static Hand getHand(int handvalue) //get hand object from int value
{
return hand[handvalue];
}
public Boolean isStrongerThan(Hand h) //if this stronger than h return true
{
return fight(h) == 1;
}
public Boolean isWeakerThan(Hand h) //if this weaker than h return true
{
return fight(h) == -1;
}
private int fight(Hand h) //thie vs h
{
if (this == h) //if draw return 0
{
return 0;
}
else if ((this.handvalue + 1) % 3 == h.handvalue) //if this won return 1
{
return 1;
}
else //if h won return -1
{
return -1;
}
}
public override String ToString() //toString method
{
return name[handvalue];
}
}
#Strategyインターフェース
じゃんけんの戦略のためのインターフェイスです。
nextHand()は次の一手を決定するメソッドです。
study()は前回の対戦結果を記録するメソッドです。
public interface Strategy
{
Hand nextHand(); //decide next move
void study(Boolean win); //memorize previous move
}
#WonStrategyクラス
Strategyインターフェースを実装するクラスです。
このクラスのじゃんけん戦略は、前に勝った手をもう一度出し、負けた場合はランダムで出すというものです。
using System;
public class WonStrategy : Strategy //strategy that chooses next move which won previously
{
private Random rand;
private Boolean won = false;
private Hand prevHand;
public WonStrategy(int seed)
{
rand = new Random(seed);
}
public Hand nextHand()
{
if(!won)
{
prevHand = Hand.getHand(rand.Next(3));
}
return prevHand;
}
public void study(Boolean win)
{
won = win;
}
}
#ProbStrategyクラス
Strategyインターフェースを実装するクラスです。
このクラスのじゃんけん戦略は、過去の勝ち負けの履歴を記録しそこから確率を出して次の一手を決定するというものです。
詳細な確率の求め方等の説明については以下の本を参照してください。
「Java言語で学ぶデザインパターン入門」
using System;
public class ProbStrategy : Strategy
{
private Random rand;
private int prevHandValue = 0;
private int currentHandValue = 0;
private int[][] history =
{
new int [] {1, 1, 1, },
new int [] {1, 1, 1, },
new int [] {1, 1, 1, },
};
public ProbStrategy(int seed)
{
rand = new Random(seed);
}
public Hand nextHand()
{
int bet = rand.Next(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クラス
Playerクラスは、じゃんけんを行う人を表現したクラスです。
Playerクラスは「名前」と「戦略」を与えられてインスタンスを作成します。
Playerクラスは勝敗結果を保持し、それを利用して次の一手の決定戦略を各Strategy実装クラスに委譲しています。
using System;
public class Player
{
private String name;
private Strategy strategy;
private int wincount;
private int losecount;
private int gamecount;
public Player(String name, Strategy strategy)//creates a player with choosen strategy
{
this.name = name;
this.strategy = strategy;
}
public Hand nextHand()//ask strategy for next hand move
{
return strategy.nextHand();
}
public void win()//match result is win
{
strategy.study(true);
wincount++;
gamecount++;
}
public void lose()//match result is lose
{
strategy.study(true);
losecount++;
gamecount++;
}
public void even()//match result is draw
{
gamecount++;
}
public override String ToString()
{
return "[" + name + ":" + gamecount + "games, " + wincount + "win, " + losecount + " lose" + "]";
}
}
#StrategyPatternクラス
実際に二人のプレイヤーを対戦させその結果をコンソールに表示させるクラスです。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static System.Net.Mime.MediaTypeNames;
namespace StrategyPattern
{
class StrategyPattern
{
static void Main(string[] args)
{
if(args.Length != 2)
{
Console.WriteLine("Usage: StrategyPattern randomseed1 randomseed2");
Console.WriteLine("Example: StrategyPattern 314 15");
Environment.Exit(0);
}
int seed1 = int.Parse(args[0]);
int seed2 = int.Parse(args[1]);
Player player1 = new Player("Taro", new WonStrategy(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))
{
Console.WriteLine("Winner:" + player1);
player1.win();
player2.lose();
}
else if (nextHand2.isStrongerThan(nextHand1))
{
Console.WriteLine("Winner:" + player2);
player1.lose();
player2.win();
}
else
{
Console.WriteLine("Even...");
player1.even();
player2.even();
}
}
Console.WriteLine("Total result:");
Console.WriteLine(player1);
Console.WriteLine(player2);
Console.ReadLine();
}
}
}
#実行結果
実行結果は以下のようになります。
※ランダム要素があるため、勝敗の結果や勝敗数が異なる場合があります。
Even...
Winner[Hana:1 games, 0 win, 0 lose]
Winner[Taro:2 games, 0 win, 1 lose]
・
・
・
(中略)
・
・
・
Winner[Taro:9998 games, 5874 win, 4110 lose]
Winner[Hana:9999 games, 4110 win, 5875 lose]
Total result:
[Taro:10000 games, 5875 win, 4111 lose]
[Hana:10000 games, 4111 win, 5875 lose]
#考察
Strategyパターンを使用してアルゴリズムをプログラムから切り離して設計し管理することでアルゴリズムのメンテナンスを容易にしリサイクル性を向上させることができる上に、プログラム実行時に動的にアルゴリズムを切り替えるようなこともできます。
ところで、Strategy(アルゴリズム)部分を委譲させて切り替えるという方法は、C#でいえば関数オブジェクトを作成するデリゲート、C,C++では関数ポインタに少し似ているかもしれません。
以下にデリゲートとStrategyパターンとの違いをまとめます。
Strategyパターンはアルゴリズムをクラスでカプセル化するため以下のようなことができます。
<クラス変数を持つことができるためアルゴリズム自身にステートを持たせることができる>
今回のじゃんけんの例でいえば、前の結果を保持し、そこから次の一手を決定しています。デリゲートでも実装は可能ですが、ステートを持た せるためには外部で独自に保持する機構を使用者が考慮する必要があります。
<クラスであるため二つ以上の関連したクラスメソッドを定義できる>
今回の例でいえば、NextHand()とStudy()と二つのメソッドを定義し処理しています。
デリゲートではひとつのメソッドで完結してしまうため、複数の関連したメソッドで複雑な処理を行う場合は使用者が考慮する必要があります。
このようにステートを持たせたり、複雑な処理をする必要がある場合はStrategyパターンを使用したほうがいいでしょう。