Strategyパターン

増補改訂版Java言語で学ぶデザインパターン入門

増補改訂版Java言語で学ぶデザインパターン入門

StrategyパターンのjavaサンプルコードをMooseで書いてみた。

サンプルは、じゃんけんの手を出す戦略をstrategyパターンで書いている。

まずHandクラス。

package Hand;
use Moose;
use MooseX::ClassAttribute;
use Jcode;

use constant {
    HANDVALUE_GUU => 0,
    HANDVALUE_CHO => 1,
    HANDVALUE_PAA => 2,
};

class_has hand => (
    is => 'rw',
    isa => 'ArrayRef',
    default => sub {
	return [
	    Hand->new( handvalue => HANDVALUE_GUU ),
	    Hand->new( handvalue => HANDVALUE_CHO ),
	    Hand->new( handvalue => HANDVALUE_PAA ),
	    ];
    }
    );

has name => (
    is => 'ro',
    isa => 'ArrayRef',
    default => sub {
	return [
	    'グー',
	    'チョキ',
	    'パー',
	    ];
    }
    );

has handvalue => (
    is => 'rw',
    isa => 'Int',
    required => 1,
    );

sub getHand {
    my $handvalue = shift;
    return __PACKAGE__->hand->[$handvalue];
}

sub isStrongerThan {
    my $self = shift;
    my $h = shift;

    return ($self->fight($h) == 1);
}

sub isWeakerThan {
    my $self = shift;
    my $h = shift;

    return ($self->fight($h) == -1);
}

sub fight {
    my $self = shift;
    my $h = shift;

    if ($self->{handvalue} == $h->{handvalue}) {
	return 0;
    } 
    elsif (($self->{handvalue} + 1) % 3 == $h->{handvalue}) {
	return 1;
    }
    else {
	return -1;
    }
}

sub toString {
    my $self = shift;
    return jcode($self->name->[$self->{handvalue}])->euc;
}

1;

ここでは「MooseX::ClassAttribute」を使って、下記のようにしてクラス変数を定義した。

class_has hand => (
    is => 'rw',
    isa => 'ArrayRef',
    default => sub {
	return [
	    Hand->new( handvalue => HANDVALUE_GUU ),
	    Hand->new( handvalue => HANDVALUE_CHO ),
	    Hand->new( handvalue => HANDVALUE_PAA ),
	    ];
    }
    );

次にStrategyインタフェースを定義し、これを継承した「WinningStrategy」と「ProbStrategy」を定義する。

package Strategy;
use Moose::Role;
use Hand;

requires qw/nextHand study/;

package WinningStrategy;
use Moose;
use Hand;
with 'Strategy';

has won => (
    is => 'rw',
    isa => 'Int',
    default => 0,
    );

has prevHand => (
    is => 'rw',
    isa => 'Object',
    );
 
sub nextHand {
    my $self = shift;

    if (!$self->{won}) {
	my $rand = int(rand 3);
	$self->{prevHand} = Hand::getHand($rand);

    }

    return $self->{prevHand};
}
    
sub study {
    my $self = shift;
    my $win = shift;

    $self->{won} = $win;
}

package ProbStrategy;
use Moose;
use Hand;
with Strategy;

has prevHandValue => (
    is => 'rw',
    isa => 'Int',
    default => 0,
    );

has currentHandValue => (
    is => 'rw',
    isa => 'Int',
    default => 0,
    );

has history => (
    is => 'rw',
    isa => 'ArrayRef',
    default => sub {
	return [
	    [ 1, 1, 1, ],
	    [ 1, 1, 1, ],
	    [ 1, 1, 1, ],
	    ];
    },
    );

sub nextHand {
    my $self = shift;
    
    my $bet = rand getSum($self,$self->currentHandValue);
    my $handvalue = 0;

    if ($bet < $self->history->[$self->currentHandValue]->[0]) {
	$handvalue = 0;
    }
    elsif ($bet < $self->history->[$self->currentHandValue]->[0] + $self->history->[$self->currentHandValue]->[1]) {
	$handvalue = 1;
    } 
    else {
	$handvalue = 2;
    }

    $self->{prevHandValue} = $self->{currentHandValue};
    $self->{currentHandValue} = $handvalue;
    return Hand::getHand($handvalue);
}

sub getSum {
    my $self = shift;
    my $hv = shift;

    my $sum = 0;
    for (my $i = 0; $i < 3; $i++) {
	$sum += $self->history->[$hv]->[$i];
    }
    
    return $sum;
}

sub study {
    my $self = shift;
    my $win = shift;

    if ($win) {
	$self->history->[$self->prevHandValue]->[$self->currentHandValue]++;
    } else {
	$self->history->[$self->prevHandValue]->[($self->currentHandValue + 1) % 3]++;
	$self->history->[$self->prevHandValue]->[($self->currentHandValue + 2) % 3]++;
    }
}
    
1;

WinningStrategyは、「勝ったときに出した手を、次にまた出す。勝てなかったら、次はランダムに手を出す」といった戦略になっている。
ProbStrategyは、「過去の勝ち負けの履歴を使って、それぞれの手を出す確率を変える」といった戦略になっている。

次にPlayerクラス。

package Player;
use Moose;
use Strategy;

has name => (
    is => 'rw',
    isa => 'Str',
    required => 1,
    );

has strategy => (
    is => 'rw',
    isa => 'Object',
    required => 1,
    );

has wincount => (
    is => 'rw',
    isa => 'Int',
    default => 0,
    );

has losecount => (
    is => 'rw',
    isa => 'Int',
    default => 0,
    );

has gamecount => (
    is => 'rw',
    isa => 'Int',
    default => 0,
    );

sub nextHand {
    my $self = shift;
    return $self->strategy->nextHand();
}

sub win {
    my $self = shift;

    $self->strategy->study(1);
    $self->{wincount}++;
    $self->{gamecount}++;
}

sub lose {
    my $self = shift;
    
    $self->strategy->study(0);
    $self->{losecount}++;
    $self->{gamecount}++;
}

sub even {
    my $self = shift;

    $self->{gamecount}++;
}

sub toString {
    my $self = shift;
    return '[' . $self->{name} . ':' . $self->{gamecount} . 'games, ' . $self->{wincount} . ' win, ' . $self->{losecount} . ' lose' . ']';
}

1;

Playerは、名前と戦略が渡されてインスタンスが生成される。このインスタンス生成時に戦略が切り替えられる。
以下が、実行プログラムと実行結果。

#!/usr/bin/perl
use strict;
use warnings;
use Player;
use Strategy;

my $player1 = Player->new( name => 'Taro', strategy => WinningStrategy->new);
my $player2 = Player->new( name => 'Hana', strategy => ProbStrategy->new);

for (my $i = 0; $i < 10000; $i++) {

    my $nextHand1 = $player1->nextHand();
    my $nextHand2 = $player2->nextHand();

    if ($nextHand1->isStrongerThan($nextHand2)) {
	print 'Winner:' . $player1->name . "\n";
	$player1->win();
	$player2->lose();
    } elsif ($nextHand2->isStrongerThan($nextHand1)) {
	print 'Winner:' . $player2->name . "\n";
	$player1->lose();
	$player2->win();
    } else {
	print "Even...\n";
	$player1->even();
	$player2->even();
    }

    print "Total result:\n";
    print $player1->toString() . "\n";
    print $player2->toString() . "\n";
}

exit;

〜略〜

Total result:
[Taro:9996games, 3091 win, 3494 lose]
[Hana:9996games, 3494 win, 3091 lose]
Winner:Hana
Total result:
[Taro:9997games, 3091 win, 3495 lose]
[Hana:9997games, 3495 win, 3091 lose]
Winner:Hana
Total result:
[Taro:9998games, 3091 win, 3496 lose]
[Hana:9998games, 3496 win, 3091 lose]
Winner:Hana
Total result:
[Taro:9999games, 3091 win, 3497 lose]
[Hana:9999games, 3497 win, 3091 lose]
Even...
Total result:
[Taro:10000games, 3091 win, 3497 lose]
[Hana:10000games, 3497 win, 3091 lose]

デザインパターンの概要

戦略ごとに「WinningStrategy」と「ProbStrategy」が定義された。これらのクラスは「Strategy」クラスを継承することで、インターフェースを統一された。例では「nextHand()」と「study()」がインタフェースだった。戦略クラスを利用する「Player」クラスは、

$strategy->nextHand();
$strategy->study();

で呼び出している。ここで「$strategy」に、「WinningStrategy」のインスタンスか「ProbStrategy」のインスタンスを入れることによって、じゃんけんの戦略が切り替えられるようになっている。

参考文献