2010年12月6日月曜日

第19章 Stateパターン : 状態をクラスとして表現する



今回はStateパターン。なかなかエキセントリックなデザインパターンです。

要は
  • メソッドの実装をある状態に応じて切り替える(条件分岐させる)
ような実装を
  • ある状態というクラスがメソッドの実装を記述する
という、説明しても『???』なデザインパターンです。

もう少し概念的な説明すると、

  • 普通の条件分岐…『この状態ならこういう処理にする』
  • Stateパターン…『この状態こういう処理をする』

というように、状態が主語になるイメージです。


いつ使うのか。メリットは何か。

使う場面を間違えると却って混乱しそうなStateパターン。自分で例を考えて実装してみて、有効だと感じた点を列挙します。

  1. どのメソッドにも同じような条件分岐が散らばるほどに重要な『状態』がある時
    まあこれは当たり前っちゃあ当たり前。『この状態の時に呼ばれたらこう動く』という説明のメソッドがあったら要チェック。

  2. 将来状態が増減しそうな時
    これも当たり前かな

  3. 状態を矛盾なく取り扱える
    本書でも触れてますが、このパターンを適用する時は各Stateは排反である必要があるので、必竟自己矛盾が起きません
    これはプログラミングする上でとても大事です。『このifはelse ifにしないと動かない』みたいな分かりづらさから解放されます。
逆にデメリットは以下のような点。
  1. 状態が増えすぎるとしんどい
    特にベクトルの違う複数の状態の組み合わせだと、状態数が指数的に増えるおそれがある




今回は完全オリジナル。どこかのブログのパクリで、『マリオでStateパターン』です。
1ターンにランダムで

  • きのこを食べる
  • フラワーを食べる
  • スターを食べる(スターは3ターンで切れる)
  • 敵に当たる
  • 穴に落ちる

のなかから一つの行動をします。State(状態)は

  • チビマリオ
  • デカマリオ
  • フラワーマリオ
  • 上記3種のスター状態

の計6種類です。
これだと、例えば『しっぽマリオを追加する』なんて時にSippoMarioStateクラスとStarSippoMarioStateクラスを追加するだけで実現できてしまいます(他所はいじる必要なし!)。Stateパターンの有効なケースですね。


実行結果
[残5:チビマリオ] スターを食べた!  チビマリオ★ になった!
[残5:チビマリオ★] ...
[残5:チビマリオ★] キノコを食べた!  デカマリオ★ になった!
[残5:デカマリオ★] スターが切れた!  デカマリオ になった!
[残5:デカマリオ] スターを食べた!  デカマリオ★ になった!
[残5:デカマリオ★] 敵に接触!  敵を倒した!
[残5:デカマリオ★] スターを食べた!  デカマリオ★ になった!
[残5:デカマリオ★] 穴に落ちた!  死んだ! 残り残機:4
[残4:チビマリオ] キノコを食べた!  デカマリオ になった!
[残4:デカマリオ] ...
[残4:デカマリオ] 穴に落ちた!  死んだ! 残り残機:3
[残3:チビマリオ] ...
[残3:チビマリオ] フラワーを食べた!  デカマリオ になった!
[残3:デカマリオ] スターを食べた!  デカマリオ★ になった!
[残3:デカマリオ★] フラワーを食べた!  ファイアーマリオ★ になった!
[残3:ファイアーマリオ★] フラワーを食べた!  意味はない!
[残3:ファイアーマリオ★] スターが切れた!  ファイアーマリオ になった!
[残3:ファイアーマリオ] ...
[残3:ファイアーマリオ] 穴に落ちた!  死んだ! 残り残機:2
[残2:チビマリオ] フラワーを食べた!  デカマリオ になった!
[残2:デカマリオ] 敵に接触!  チビマリオ になった!
[残2:チビマリオ] 穴に落ちた!  死んだ! 残り残機:1
[残1:チビマリオ] キノコを食べた!  デカマリオ になった!
[残1:デカマリオ] スターを食べた!  デカマリオ★ になった!
[残1:デカマリオ★] フラワーを食べた!  ファイアーマリオ★ になった!
[残1:ファイアーマリオ★] キノコを食べた!  意味はない!
[残1:ファイアーマリオ★] スターが切れた!  ファイアーマリオ になった!
[残1:ファイアーマリオ] フラワーを食べた!  意味はない!
[残1:ファイアーマリオ] 敵に接触!  チビマリオ になった!
[残1:チビマリオ] フラワーを食べた!  デカマリオ になった!
[残1:デカマリオ] 敵に接触!  チビマリオ になった!
[残1:チビマリオ] 敵に接触!  死んだ! 残り残機:0
[残0:チビマリオ] 穴に落ちた!  GAME OVER
30ターンプレイしました

ソースコード
#!/usr/bin/env python
# -*- coding: utf8 -*-

class __MarioState:
    def __init__(self): raise NotImplementedError
    def doGetKinoko(self, context): raise NotImplementedError # キノコ取得
    def doGetFlower(self, context): raise NotImplementedError # フラワー取得
    def doGetStar(self, context): raise NotImplementedError # スター取得
    def doLostStar(self, context): raise NotImplementedError # スター消滅
    def doEncounter(self, context): raise NotImplementedError # 敵に接触
    def doFall(self, context): raise NotImplementedError # 穴に落ちる
    def __str__(self): raise NotImplementedError


class NormalMario(__MarioState):
    def __init__(self): pass
    def doGetKinoko(self, context): context.changeState(BigMario())
    def doGetFlower(self, context): context.changeState(BigMario())
    def doEncounter(self, context): context.dead()
    def doGetStar(self, context): context.changeState(StarNormalMario())
    def doLostStar(self, context): print "bug"
    def doFall(self, context): context.dead()
    def __str__(self): return "チビマリオ"

class BigMario(__MarioState):
    def __init__(self): pass
    def doGetKinoko(self, context): context.nope()
    def doGetFlower(self, context): context.changeState(FlowerMario())
    def doEncounter(self, context): context.changeState(NormalMario())
    def doGetStar(self, context): context.changeState(StarBigMario())
    def doLostStar(self, context): print "bug"
    def doFall(self, context): context.dead()
    def __str__(self): return "デカマリオ"

class FlowerMario(__MarioState):
    def __init__(self): pass
    def doGetKinoko(self, context): context.nope()
    def doGetFlower(self, context): context.nope()
    def doEncounter(self, context): context.changeState(NormalMario())
    def doGetStar(self, context): context.changeState(StarFlowerMario())
    def doLostStar(self, context): print "bug"
    def doFall(self, context): context.dead()
    def __str__(self): return "ファイアーマリオ"

class StarNormalMario(__MarioState):
    def __init__(self): pass
    def doGetKinoko(self, context): context.changeState(StarBigMario())
    def doGetFlower(self, context): context.changeState(StarBigMario())
    def doGetStar(self, context): context.changeState(StarNormalMario())
    def doLostStar(self, context): context.changeState(NormalMario())
    def doEncounter(self, context): context.defeat()
    def doFall(self, context): context.dead()
    def __str__(self): return "チビマリオ★"

class StarBigMario(__MarioState):
    def __init__(self): pass
    def doGetKinoko(self, context): context.nope()
    def doGetFlower(self, context): context.changeState(StarFlowerMario())
    def doGetStar(self, context): context.changeState(StarBigMario())
    def doLostStar(self, context): context.changeState(BigMario())
    def doEncounter(self, context): context.defeat()
    def doFall(self, context): context.dead()
    def __str__(self): return "デカマリオ★"

class StarFlowerMario(__MarioState):
    def __init__(self): pass
    def doGetKinoko(self, context): context.nope()
    def doGetFlower(self, context): context.nope()
    def doGetStar(self, context): context.changeState(StarFlowerMario())
    def doLostStar(self, context): context.changeState(FlowerMario())
    def doEncounter(self, context): context.defeat()
    def doFall(self, context): context.dead()
    def __str__(self): return "ファイアーマリオ★"



class __Context:
    def __init__(self): raise NotImplementedError
    def changeState(self, mariostate): raise NotImplementedError # 状態が変化した
    def defeat(self): raise NotImplementedError # 敵を倒した
    def dead(self): raise NotImplementedError # 死んだ
    def nope(self): raise NotImplementedError # 何も起きない

import sys
from threading import Timer
class Game(__Context):
    def __init__(self):
        self.state = NormalMario()
        self.zanki = 5 # 残機
        self.turncount = 0 # 何ターン目か
        self.starcount = 99999 # スター用カウンタ

    def changeState(self, mariostate):
        self.state = mariostate
        print str(mariostate)+" になった!"

    def defeat(self):
        print "敵を倒した!"

    def dead(self):
        self.zanki -= 1
        if self.zanki < 0:
            print "GAME OVER"
            print "%dターンプレイしました" % self.turncount
            sys.exit(0)
        print "死んだ! 残り残機:%d" % self.zanki
        self.state = NormalMario()
        self.starcount = 99999

    def nope(self):
        print "意味はない!"

    #--

    def youGetKinoko(self):
        print "[残%d:%s] キノコを食べた! " % (self.zanki, self.state),
        self.state.doGetKinoko(self)
    def youGetFlower(self):
        print "[残%d:%s] フラワーを食べた! " % (self.zanki, self.state),
        self.state.doGetFlower(self)
    def youEncounter(self):
        print "[残%d:%s] 敵に接触! " % (self.zanki, self.state),
        self.state.doEncounter(self)
    def youFall(self):
        print "[残%d:%s] 穴に落ちた! " % (self.zanki, self.state),
        self.state.doFall(self)
    def nothingHappened(self):
        print "[残%d:%s] ..." % (self.zanki, self.state)

    def youGetStar(self):
        print "[残%d:%s] スターを食べた! " % (self.zanki, self.state),
        self.state.doGetStar(self)
        self.starcount = 0
    def __youLostStar(self):
        print "[残%d:%s] スターが切れた! " % (self.zanki, self.state),
        self.state.doLostStar(self)

    def youCountTurn(self):
        self.turncount += 1
        self.starcount += 1
        if self.starcount == 3:
            self.__youLostStar()



from time import sleep
from random import randint
def main():
    m_game = Game()
    while True:
        # random action
        m_game.youCountTurn()
        act = randint(1,6)
        if   act==1: m_game.youGetKinoko()
        elif act==2: m_game.youGetFlower()
        elif act==3: m_game.youGetStar()
        elif act==4: m_game.youEncounter()
        elif act==5: m_game.youFall()
        elif act==6: m_game.nothingHappened()
        sleep(0.1)

if __name__=='__main__': main()

0 件のコメント:

コメントを投稿