LoginSignup
6
1

More than 1 year has passed since last update.

Stateパターンのアンチパターンを考える

Last updated at Posted at 2022-05-30

はじめに

Stateパターンのアンチパターンをいくつか考えたので紹介します.

Stateパターンとは

GoFのデザパタのあれです.
ここでは解説しません.

本題

他Stateへの依存

状態遷移を行う際,Stateクラス内部から次のStateを指定する必要があります.
しかし肝心のStateはどこから持ってくれば良いのでしょうか?

Singleton

Stateパターンについて調べるとよく出てくるのが,StateクラスをSingletonにしてしまう方法です.

この方法は,Stateクラスが内部状態を持つ場合,複数のContextが利用できなくなってしまいます.
複数のContextは,ゲームプログラミングで例を考えるとモンスターやプレイヤーなどのStateでしょうか.

また,後述しますがStateクラスが他のStateクラスにアクセスするのはあまりよろしくありません.

新規生成

Singletonのデメリットである,複数のContextで利用できない,が解決出来ます.

しかし,遷移ごとにインスタンス化されるというのは,パフォーマンス上あまりよくなさそうです.
同一Context内ならオブジェクトを再利用したいところです.

さらに,新規生成するStateクラスの依存クラスに自分自身も依存してしまうことになります.

DI

だったらContextから次のStateを受け取ればいいのではないでしょうか?

これでContext内でStateクラスがSingletonのように振る舞うようになり,再利用が可能になりました.
また不要な依存クラスができてしまう問題も解決できそうです.

ただし,Stateがループする場合,コンストラクタインジェクションが不可能になるため,工夫が必要です.

これで解決なのか?

Singletonのところで少し触れましたが,実は上記の方法ではもう一つ問題が残っています.

複雑な依存関係

遷移時にContextへStateインスタンスを伝えるということは,そのクラスに依存していることになります.
つまり,遷移=依存になります.
image.png

遷移先として指定しているだけとは言え,依存関係が滅茶苦茶な立派なスパゲッティ状態です.

ちなみに,抽象StateをDIすればいいんじゃないの?と思う方もいるかもしれません.
確かに依存関係はクリーンになります.
image.png
しかしその場合,State遷移の裁量権がContextに移ることになります.
ContextがStateを注入するためです.Stateパターンとは?

(遷移先が動的に入れ替わる,みたいなことをしたいならありかも)

あと,遷移先をIStateAのように抽象化する方法も思いつきましたが,そもそも他のStateクラスを操作できる時点でダメです.

解決方法

ここまでの方法で問題の根本的な原因は,Stateの種類とStateクラスそのものが一緒になっていることです.

StateA -> StateBという遷移を考えましょう.StateAはContextに,StateBに遷移することを伝えたいのですが,このときStateAにとってStateBは,StateBという種類(Type)が欲しいのであり,StateBそのものはどうだっていいんです.
image.png

つまりどうすればいいのか.
StateTypeとしてStateの型をEnumにしてしまいましょう.
image.png

これで全てうまくいきます(多分).

ちなみにStateTypeとStateインスタンスの紐づけはContextImplが行います.
しかし,これを聞いて1つ疑問が生まれるかもしれません.

いやいや...結局State遷移の裁量権はContextが持ってんじゃないの?

そうかもしれません.
でも,StateType.Aを伝えてるのにStateBに遷移するっておかしな話じゃないですか?

この場合はStateクラス自身が遷移先を明示しているので,いいんじゃないかなと思います.

各Stateの振る舞い

Stateパターンの本命と言える部分です.
しかし,ここに大きなアンチパターンを発見しました.
ここでは,ゲームを例に考えていきます.

話にならない実装例

Stateパターンと検索すると,このような実装例が出てきがちです.

class Player {

    private var currentState: IState

    fun Damage() {
        currentState.onDamage()
    }

}

interface IState {

    fun onDamage()

}

class StateNormal : IState {

    override fun onDamage() {
        println("いてて..")
    }

}

class StateInvincible : IState {

    override fun onDamage() {
        println("無敵だもん!")
    }

}

「PlayerクラスはIStateのonDamageを呼ぶだけで場合分けをしなくていいね!」
「ポリモーフィズムだね!」

...

Stateパターン実装してみれば分かります.実際のところ,

  • プレイヤーのHP
  • 何の攻撃を受けたか
  • ダメージの演出
  • 無敵時間の処理

など,沢山の要素への依存があり,一筋縄ではいかないです.

命令的に書くな

Stateパターンと言えば、何らかのイベント(onDamageなど)に対して,Contextのメソッドを呼ぶ実装方法をよく見かけます.
しかしこれは,アンチパターンになり得ると思っています.

まずやりがちな実装を見てみましょう.先程の例をもう少し実践的にして考えます.

  • NormalState,FixedStateではダメージを受けるとHPが減り,数秒間無敵状態になる
  • FixedStateではノックバックはしない
  • InvincibleStateではHPは減らないがノックバックだけする

これらを満たすStateクラスたちを実装してみます.

interface PlayerContext {
    var hp: Int
    var mp: Int
    var invincibleTime: Double
    var velocity: Vector
    fun changeStateTo(next: StateType)
}

class StateNormal(private val context: PlayerContext) : IState {
    
    fun onDamage() {
        // 体力を減らす
        context.hp -= 10
        // ノックバック
        context.velocity = Vector(1.0, 1.0, 1.0)
        // 無敵時間
        context.invincibleTime = 10.0
        context.changeStateTo(StateType.Invincible)
    }

}
class StateFixed(private val context: PlayerContext) : IState {
    
    fun onDamage() {
        // 体力を減らす
        context.hp -= 10
        // 無敵時間
        context.invincibleTime = 10.0
        context.changeStateTo(StateType.Invincible)
    }

}
class StateInvincible(private val context: PlayerContext) : IState {
    
    fun onDamage() {
        // ノックバック
        context.velocity = Vector(1.0, 1.0, 1.0)
    }

}

一見すると,いい感じのStateパターンになってるように思えます.
(PlayerContextが全然カプセル化されてないですが)

しかし,Stateクラスの振る舞いは宣言的であるべきとぼくは考えています.
すなわち,Contextに対して複雑な処理をすべきではないということです.

責任が増える

上の例でStateクラスの責務をよく考えてみましょう.
ダメージを受けたときに「HPが減るか」「無敵状態になるか」「ノックバックするか」

これらの宣言だけをさせたかったんです

しかしながら,「HPの減少量」「無敵時間の長さ」「ノックバック力」など
Stateクラスにとってどうでもいいことまで把握してしまっています.

より実践的なものなら,引数から「ダメージ量」「攻撃方向」などの情報を受け取って,「プレイヤーの装備」「防御力」からHP減少量の計算を行ったりするかもしれません.

これらはStateクラスが果たすべき責務ではありませんよね.

宣言的に

今度は,Stateクラスが宣言的になるよう実装してみます.

interface IState {
    
    val knockBackOnDamage: Boolean
    val isDamageable: Boolean
    
}

class StateNormal() : IState {

    override val knockBackOnDamage: Boolean = true
    override val isDamageable: Boolean = true
    
}

class StateFixed() : IState {

    override val knockBackOnDamage: Boolean = false
    override val isDamageable: Boolean = true

}

class StateInvincible() : IState {

    override val knockBackOnDamage: Boolean = true
    override val isDamageable: Boolean = false

}

Stateを利用する側は次のようになります.

class Player {
    
    private var state: IState
    private var hp: Int
    private var velocity: Vector
    
    fun damage() {
        
        if (state.isDamageable) {
            
            hp -= 10
            
        }
        
        if (state.knockBackOnDamage) {
            
            velocity = Vector(1.0, 1.0, 1.0)
            
        }
        
    }
    
}

これで,Stateクラスの振る舞いを宣言的にすることができました.

また,「Stateごとにダメージ量やらノックバック力を変えたい」
となった場合でも,Stateの振る舞いに「ダメージ倍率」なり「ダメージ計算メソッド」なりを付け加えてやればOKです.

最後に

本質はStateパターンじゃない

ぼくはこの記事でStateパターンはこうだ!といいたいわけではありません.

ぼくの中でのオブジェクト指向に基づき,Stateパターンを考えたらこうなった,というのが趣旨となります.
ですので,オブジェクト指向の考え方を参考にしていただければなと思います.

Twitterフォローしてください

普段こういう系のツイートしてるので,興味のある方はフォローお願いします.
誰も絡んでくれなくて悲しいです.

参考

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1