振る舞いに関するパターン
Strategy
アルゴリズムをごっそり切り替える
概要
データ構造に対して適用する一連のアルゴリズムをカプセル化し、アルゴリズムの切替えを容易にする。
マンガでわかる Strategy
マンガでわかる Strategy #デザインパターン - Qiita
でざぱたんで覚える Strategy
ちびキャラは「ストラテジーたん」、ステートたん(State)の双子の兄。内に状態を抱え込まず、その場その場で「やり方」だけを差し替えて同じ顔で応対する話術師。本書の整理では、StrategyはStateから「状態をオブジェクトで管理する」を省いて、関数オブジェクトによるポリモーフィズムに特化した考え方——妹と同じ顔(同一インターフェース)で、中身のアルゴリズムだけ交換可能、という対比で双子として描かれる。
出典: いしだけ『でざぱたん: ちびキャラで覚えるデザインパターン』(P.014〜)
登場人物
- Strategy: 戦略名インタフェース
- ConcreteStrategy: 戦略を実装 (具体的な作戦、方策、方法、アルゴリズムの切り替え)
- Context: Strategy利用者
クラス図
このサイトの実装(じゃんけん)での対応関係:
classDiagram
class Strategy {
<<interface>>
+nextHand() Hand
+study(win)
}
class WinningStrategy
class ProbStrategy
class Player {
+nextHand() Hand
+win()
+lose()
+even()
}
Strategy <|.. WinningStrategy
Strategy <|.. ProbStrategy
Player o-- Strategy
やり方
- ConcreteStrategy達にStrategyを実装させる
- ContextはConcreteStrategyインスタンスを持つが、Strategyインタフェースを呼び出す
なんのクラスを呼び出すかについての指定は、リフレクションにより動的に行うことで、Contextはクラス名を知る必要がなくなる
メリット(用途)
所感
これただのインタフェースの使い方では?
Java
ProbStrategy.java
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]++;
}
}
}
Strategy.java
public interface Strategy {
public abstract Hand nextHand();
public abstract void study(boolean win);
}
WinningStrategy.java
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) {
prevHand = Hand.getHand(random.nextInt(3));
}
return prevHand;
}
public void study(boolean win) {
won = win;
}
}
Hand.java
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];
}
}
Player.java
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.java
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());
}
}
Go
Java版と同じ題材(じゃんけん)を移植。interface を暗黙実装する点、Hand を静的インスタンス配列でなく型付き定数(enum風)+Stringerで表す点がGoらしい差分。
実行: go run ./GoF/patterns/Strategy/go [seed1 seed2]
$ go run ./GoF/patterns/Strategy/go
strategy.go
package main
// Strategy はじゃんけんの「戦略」を表すインタフェース。
// ConcreteStrategy(WinningStrategy / ProbStrategy)がこれを実装する。
//
// Go の interface は暗黙実装(implements を書かない)。
// ここが Java との一番わかりやすい違い。
type Strategy interface {
NextHand() Hand // 次の手を返す
Study(win bool) // 直前の手が勝ったか負けたかを学習する
}
hand.go
package main
// Hand はじゃんけんの手を表す。
// Java版は Hand の静的インスタンス配列で表現していたが、
// Go では「型付き定数(enum風)」+ Stringer で自然に書ける。
type Hand int
const (
Guu Hand = iota // グー
Cho // チョキ
Paa // パー
)
var handNames = [...]string{"グー", "チョキ", "パー"}
// String は fmt.Stringer を満たす(Java の toString 相当)。
func (h Hand) String() string {
return handNames[h]
}
// IsStrongerThan は h より自分(this)が強いとき true。
func (h Hand) IsStrongerThan(other Hand) bool {
return h.fight(other) == 1
}
// IsWeakerThan は h より自分(this)が弱いとき true。
func (h Hand) IsWeakerThan(other Hand) bool {
return h.fight(other) == -1
}
// fight は引き分けなら 0、自分の勝ちなら 1、相手の勝ちなら -1。
func (h Hand) fight(other Hand) int {
switch {
case h == other:
return 0
case (int(h)+1)%3 == int(other):
return 1
default:
return -1
}
}
winning_strategy.go
package main
import "math/rand"
// WinningStrategy は ConcreteStrategy の一つ。
// 「勝ったら次も同じ手を出す。負けたら次はランダムに変える」戦略。
type WinningStrategy struct {
random *rand.Rand
won bool
prevHand Hand
}
// NewWinningStrategy はコンストラクタ相当。
// Java の new Random(seed) と対応させるため、シードから乱数源を作る。
func NewWinningStrategy(seed int64) *WinningStrategy {
return &WinningStrategy{random: rand.New(rand.NewSource(seed))}
}
func (s *WinningStrategy) NextHand() Hand {
if !s.won {
s.prevHand = Hand(s.random.Intn(3))
}
return s.prevHand
}
func (s *WinningStrategy) Study(win bool) {
s.won = win
}
prob_strategy.go
package main
import "math/rand"
// ProbStrategy は ConcreteStrategy の一つ。
// 過去の勝敗履歴(history)から、次にどの手を出すと有利かを確率的に決める戦略。
// history[前回の手][今回の手] に勝った回数を貯めていく。
type ProbStrategy struct {
random *rand.Rand
prevHandValue int
currentHandValue int
history [3][3]int
}
func NewProbStrategy(seed int64) *ProbStrategy {
return &ProbStrategy{
random: rand.New(rand.NewSource(seed)),
history: [3][3]int{
{1, 1, 1},
{1, 1, 1},
{1, 1, 1},
},
}
}
func (s *ProbStrategy) NextHand() Hand {
bet := s.random.Intn(s.getSum(s.currentHandValue))
var handValue int
switch {
case bet < s.history[s.currentHandValue][0]:
handValue = 0
case bet < s.history[s.currentHandValue][0]+s.history[s.currentHandValue][1]:
handValue = 1
default:
handValue = 2
}
s.prevHandValue = s.currentHandValue
s.currentHandValue = handValue
return Hand(handValue)
}
// getSum は history[hv] の合計(=賭けの母数)を返す。
func (s *ProbStrategy) getSum(hv int) int {
sum := 0
for i := 0; i < 3; i++ {
sum += s.history[hv][i]
}
return sum
}
func (s *ProbStrategy) Study(win bool) {
if win {
s.history[s.prevHandValue][s.currentHandValue]++
} else {
s.history[s.prevHandValue][(s.currentHandValue+1)%3]++
s.history[s.prevHandValue][(s.currentHandValue+2)%3]++
}
}
player.go
package main
import "fmt"
// Player は Strategy の利用者(Context)。
// 自分がどんな戦略を持っているかは Strategy インタフェース越しにしか触らない。
// = 戦略の中身を差し替えても Player のコードは変わらない、が Strategy パターンの肝。
type Player struct {
name string
strategy Strategy
winCount int
loseCount int
gameCount int
}
// NewPlayer は名前と戦略を授けられる。
func NewPlayer(name string, strategy Strategy) *Player {
return &Player{name: name, strategy: strategy}
}
// NextHand は戦略におうかがいを立てる。
func (p *Player) NextHand() Hand {
return p.strategy.NextHand()
}
func (p *Player) Win() { // 勝った
p.strategy.Study(true)
p.winCount++
p.gameCount++
}
func (p *Player) Lose() { // 負けた
p.strategy.Study(false)
p.loseCount++
p.gameCount++
}
func (p *Player) Even() { // 引き分け
p.gameCount++
}
// String は fmt.Stringer を満たす(Java の toString 相当)。
func (p *Player) String() string {
return fmt.Sprintf("[%s: %d games, %d win, %d lose]", p.name, p.gameCount, p.winCount, p.loseCount)
}
main.go
package main
import (
"fmt"
"os"
"strconv"
)
// 実行:
//
// go run ./GoF/patterns/Strategy/go # デフォルトseed(314, 15)
// go run ./GoF/patterns/Strategy/go 314 15 # seed指定
//
// Java版は毎ゲームの勝者を1万行出力するが、うるさいので最終結果だけ出す。
func main() {
seed1, seed2 := int64(314), int64(15)
if len(os.Args) == 3 {
s1, err1 := strconv.ParseInt(os.Args[1], 10, 64)
s2, err2 := strconv.ParseInt(os.Args[2], 10, 64)
if err1 != nil || err2 != nil {
fmt.Println("Usage: go run ./GoF/patterns/Strategy/go randomseed1 randomseed2")
os.Exit(1)
}
seed1, seed2 = s1, s2
}
// 同じ Player でも、渡す ConcreteStrategy を差し替えるだけで挙動が変わる。
player1 := NewPlayer("Taro", NewWinningStrategy(seed1))
player2 := NewPlayer("Hana", NewProbStrategy(seed2))
for i := 0; i < 10000; i++ {
hand1 := player1.NextHand()
hand2 := player2.NextHand()
switch {
case hand1.IsStrongerThan(hand2):
player1.Win()
player2.Lose()
case hand2.IsStrongerThan(hand1):
player1.Lose()
player2.Win()
default:
player1.Even()
player2.Even()
}
}
fmt.Println("Total result:")
fmt.Println(player1)
fmt.Println(player2)
}
PHP
index.php
<?php // ストラテジパターン
/**
* Strategy 抽象的な戦略を定義する
* ConcreteStrategy Strategy 具体的な戦略を定義する
* Context ConcreteStrategyを所有する Strategyを利用する
*/
interface Strategy
{
public function behave();
}
class LadyStrategy implements Strategy
{
public function behave()
{
var_dump("本当の親のように甘える");
}
}
class OlomanStrategy implements Strategy
{
public function behave()
{
var_dump("持っているものを褒める");
}
}
class AngryManStrategy implements Strategy
{
public function behave()
{
var_dump("子供らしく振舞う");
}
}
// Context : strategy本人
class ConcreteStrategy
{
private $strategy;
public function __construct($strategy)
{
$this->strategy = $strategy;
}
public function behave()
{
$this->strategy->behave();
}
}
function main()
{
$person = new ConcreteStrategy(new LadyStrategy());
$person->behave();
$person = new ConcreteStrategy(new OlomanStrategy());
$person->behave();
$person = new ConcreteStrategy(new AngryManStrategy());
$person->behave();
}
main();TypeScript
$ npx tsx GoF/patterns/Strategy/typescript/main.ts
main.ts
// Strategy パターン: じゃんけん (Java版と同じ題材)
//
// 実行:
// npx tsx main.ts # デフォルトseed(314, 15)
// npx tsx main.ts 314 15 # seed指定
//
// Java版は毎ゲームの勝者を1万行出力するが、Go版に倣ってうるさいので最終結果だけ出す。
// Node.js の標準ライブラリにはシード指定可能な乱数がない(Math.randomはシード不可)。
// Go版のrand.NewSource(seed)相当を自前で用意する(xorshift32)。
class SeededRandom {
private state: number;
constructor(seed: number) {
// 0だとxorshiftが機能しないので下駄を履かせる
this.state = (seed >>> 0) || 0x9e3779b9;
}
// [0, bound) の整数を返す。Java の Random#nextInt(bound) / Go の rand.Intn(bound) 相当。
nextInt(bound: number): number {
this.state ^= this.state << 13;
this.state ^= this.state >>> 17;
this.state ^= this.state << 5;
this.state >>>= 0;
return this.state % bound;
}
}
// Hand: じゃんけんの手。
// TypeScriptでもJava版同様に「クラス+静的インスタンス配列」でキャッシュ表現できる。
// Go版はenum風の型付き定数で表現したので、その対比としてここはJava寄りの書き方を採用する。
class Hand {
static readonly HANDVALUE_GUU = 0;
static readonly HANDVALUE_CHO = 1;
static readonly HANDVALUE_PAA = 2;
private static readonly names = ["グー", "チョキ", "パー"] as const;
private static readonly instances: Hand[] = [0, 1, 2].map((v) => new Hand(v));
private constructor(private readonly handValue: number) {}
static getHand(handValue: number): Hand {
return Hand.instances[handValue];
}
isStrongerThan(h: Hand): boolean {
return this.fight(h) === 1;
}
isWeakerThan(h: Hand): boolean {
return this.fight(h) === -1;
}
private fight(h: Hand): number {
if (this === h) return 0;
return (this.handValue + 1) % 3 === h.handValue ? 1 : -1;
}
toString(): string {
return Hand.names[this.handValue];
}
}
// Strategy: TypeScriptのinterfaceはJavaのinterfaceとほぼ同じ感覚で書ける。
// (Go版のような暗黙実装ではなく、`implements`を明示する点はJavaと同じ)
interface Strategy {
nextHand(): Hand;
study(win: boolean): void;
}
// WinningStrategy: 勝ったら次も同じ手、負けたらランダムに変える。
class WinningStrategy implements Strategy {
private random: SeededRandom;
private won = false;
private prevHand!: Hand;
constructor(seed: number) {
this.random = new SeededRandom(seed);
}
nextHand(): Hand {
if (!this.won) {
this.prevHand = Hand.getHand(this.random.nextInt(3));
}
return this.prevHand;
}
study(win: boolean): void {
this.won = win;
}
}
// ProbStrategy: 過去の勝敗履歴から確率的に手を決める。
class ProbStrategy implements Strategy {
private random: SeededRandom;
private prevHandValue = 0;
private currentHandValue = 0;
private history: number[][] = [
[1, 1, 1],
[1, 1, 1],
[1, 1, 1],
];
constructor(seed: number) {
this.random = new SeededRandom(seed);
}
nextHand(): Hand {
const bet = this.random.nextInt(this.getSum(this.currentHandValue));
let handValue: number;
if (bet < this.history[this.currentHandValue][0]) {
handValue = 0;
} else if (bet < this.history[this.currentHandValue][0] + this.history[this.currentHandValue][1]) {
handValue = 1;
} else {
handValue = 2;
}
this.prevHandValue = this.currentHandValue;
this.currentHandValue = handValue;
return Hand.getHand(handValue);
}
private getSum(hv: number): number {
return this.history[hv].reduce((a, b) => a + b, 0);
}
study(win: boolean): void {
if (win) {
this.history[this.prevHandValue][this.currentHandValue]++;
} else {
this.history[this.prevHandValue][(this.currentHandValue + 1) % 3]++;
this.history[this.prevHandValue][(this.currentHandValue + 2) % 3]++;
}
}
}
// Player: Strategyの利用者(Context)。戦略の中身はinterface越しにしか触らない。
class Player {
private winCount = 0;
private loseCount = 0;
private gameCount = 0;
constructor(private readonly name: string, private readonly strategy: Strategy) {}
nextHand(): Hand {
return this.strategy.nextHand();
}
win(): void {
this.strategy.study(true);
this.winCount++;
this.gameCount++;
}
lose(): void {
this.strategy.study(false);
this.loseCount++;
this.gameCount++;
}
even(): void {
this.gameCount++;
}
toString(): string {
return `[${this.name}: ${this.gameCount} games, ${this.winCount} win, ${this.loseCount} lose]`;
}
}
function main(): void {
const args = process.argv.slice(2);
let seed1 = 314;
let seed2 = 15;
if (args.length === 2) {
seed1 = Number.parseInt(args[0], 10);
seed2 = Number.parseInt(args[1], 10);
if (Number.isNaN(seed1) || Number.isNaN(seed2)) {
console.log("Usage: npx tsx main.ts randomseed1 randomseed2");
process.exit(1);
}
}
const player1 = new Player("Taro", new WinningStrategy(seed1));
const player2 = new Player("Hana", new ProbStrategy(seed2));
for (let i = 0; i < 10000; i++) {
const hand1 = player1.nextHand();
const hand2 = player2.nextHand();
if (hand1.isStrongerThan(hand2)) {
player1.win();
player2.lose();
} else if (hand2.isStrongerThan(hand1)) {
player1.lose();
player2.win();
} else {
player1.even();
player2.even();
}
}
console.log("Total result:");
console.log(player1.toString());
console.log(player2.toString());
}
main();
Python
$ python3 GoF/patterns/Strategy/python/main.py
main.py
"""Strategy パターン: じゃんけん (Java版と同じ題材)
実行:
python3 main.py # デフォルトseed(314, 15)
python3 main.py 314 15 # seed指定
Java版は毎ゲームの勝者を1万行出力するが、Go/TS版に倣ってうるさいので最終結果だけ出す。
"""
from __future__ import annotations
import random
import sys
from abc import ABC, abstractmethod
class Hand:
"""じゃんけんの手。Java版同様「クラス+静的インスタンスのキャッシュ」で表現する。"""
HANDVALUE_GUU = 0
HANDVALUE_CHO = 1
HANDVALUE_PAA = 2
_names = ("グー", "チョキ", "パー")
def __init__(self, hand_value: int) -> None:
self._hand_value = hand_value
@classmethod
def get_hand(cls, hand_value: int) -> "Hand":
return _HAND_INSTANCES[hand_value]
def is_stronger_than(self, h: "Hand") -> bool:
return self._fight(h) == 1
def is_weaker_than(self, h: "Hand") -> bool:
return self._fight(h) == -1
def _fight(self, h: "Hand") -> int:
if self is h:
return 0
return 1 if (self._hand_value + 1) % 3 == h._hand_value else -1
def __str__(self) -> str:
return self._names[self._hand_value]
_HAND_INSTANCES = [Hand(0), Hand(1), Hand(2)]
class Strategy(ABC):
"""PythonにはJavaのinterfaceに相当する言語機能はないため、抽象基底クラス(ABC)で表す。
Goのような暗黙実装ではなく、Java同様に継承(Strategyを継承)して満たす必要がある。
"""
@abstractmethod
def next_hand(self) -> Hand: ...
@abstractmethod
def study(self, win: bool) -> None: ...
class WinningStrategy(Strategy):
"""勝ったら次も同じ手、負けたらランダムに変える。"""
def __init__(self, seed: int) -> None:
self._random = random.Random(seed)
self._won = False
self._prev_hand: Hand | None = None
def next_hand(self) -> Hand:
if not self._won:
self._prev_hand = Hand.get_hand(self._random.randrange(3))
assert self._prev_hand is not None
return self._prev_hand
def study(self, win: bool) -> None:
self._won = win
class ProbStrategy(Strategy):
"""過去の勝敗履歴から確率的に手を決める。"""
def __init__(self, seed: int) -> None:
self._random = random.Random(seed)
self._prev_hand_value = 0
self._current_hand_value = 0
self._history = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
def next_hand(self) -> Hand:
bet = self._random.randrange(self._get_sum(self._current_hand_value))
row = self._history[self._current_hand_value]
if bet < row[0]:
hand_value = 0
elif bet < row[0] + row[1]:
hand_value = 1
else:
hand_value = 2
self._prev_hand_value = self._current_hand_value
self._current_hand_value = hand_value
return Hand.get_hand(hand_value)
def _get_sum(self, hv: int) -> int:
return sum(self._history[hv])
def study(self, win: bool) -> None:
if win:
self._history[self._prev_hand_value][self._current_hand_value] += 1
else:
self._history[self._prev_hand_value][(self._current_hand_value + 1) % 3] += 1
self._history[self._prev_hand_value][(self._current_hand_value + 2) % 3] += 1
class Player:
"""Strategyの利用者(Context)。戦略の中身はStrategyのインタフェース越しにしか触らない。"""
def __init__(self, name: str, strategy: Strategy) -> None:
self._name = name
self._strategy = strategy
self._win_count = 0
self._lose_count = 0
self._game_count = 0
def next_hand(self) -> Hand:
return self._strategy.next_hand()
def win(self) -> None:
self._strategy.study(True)
self._win_count += 1
self._game_count += 1
def lose(self) -> None:
self._strategy.study(False)
self._lose_count += 1
self._game_count += 1
def even(self) -> None:
self._game_count += 1
def __str__(self) -> str:
return f"[{self._name}: {self._game_count} games, {self._win_count} win, {self._lose_count} lose]"
def main() -> None:
seed1, seed2 = 314, 15
if len(sys.argv) == 3:
try:
seed1, seed2 = int(sys.argv[1]), int(sys.argv[2])
except ValueError:
print("Usage: python3 main.py randomseed1 randomseed2")
sys.exit(1)
player1 = Player("Taro", WinningStrategy(seed1))
player2 = Player("Hana", ProbStrategy(seed2))
for _ in range(10000):
hand1 = player1.next_hand()
hand2 = player2.next_hand()
if hand1.is_stronger_than(hand2):
player1.win()
player2.lose()
elif hand2.is_stronger_than(hand1):
player1.lose()
player2.win()
else:
player1.even()
player2.even()
print("Total result:")
print(player1)
print(player2)
if __name__ == "__main__":
main()