State
1. 目的
状態に応じて実行するコードを分割したい
- 状態に応じて異なる処理を実行するコードは愚直に実装すると保守性の低いコードになりがち
- 状態に応じて異なる処理を実行するコードをいい感じに分割して保守性を担保したい
2. 課題
条件分岐で状態ごとのコードを記述すると長大になりがち
- 状態に応じて実行する処理を変えたい時、愚直に条件分岐で記述するとメソッドが長大になりがち
- 長大なメソッドは保守性が低下する原因になる
class Program:
def exec(self, status):
if status == "status01":
# 以下、10-20行程度の処理が書いてあるものとする
...
return "わーい"
elif status == "status02":
# 以下、10-20行程度の処理が書いてあるものとする
...
return "うぇーい"
else:
raise ValueError("未定義のstatus")
if __name__ == '__main__':
p = Program()
print(p.exec("status01")) # <= わーい
print(p.exec("status02")) # <= うぇーい
3. 解決策
処理を委譲するクラスを切り替えられるようにする
class Context:
def change_state(self, state):
""" stateを変更する """
self._state = state
def exec(self):
""" stateに委譲した処理を呼び出す """
return self._state.exec()
class State(metaclass=ABCMeta):
""" 処理を委譲されるクラスのインターフェイス """
@abstractmethod
def exec(self):
pass
class StateA(State):
""" 処理を委譲されるクラスの実装その1 """
def exec(self):
# 以下、10-20行程度の処理が書いてあるものとする
...
return "わーい"
class StateB(State):
""" 処理を委譲されるクラスの実装その2 """
def exec(self):
# 以下、10-20行程度の処理が書いてあるものとする
...
return "うぇーい"
if __name__ == '__main__':
c = Context()
c.change_state(StateA())
print(c.exec()) # <= わーい
c.change_state(StateB())
print(c.exec()) # <= うぇーい
- 状態毎に実行されるコードをクラス化し、更にそれらのコードを実行するクラスに分割した
- 状態毎に実行されるクラス(
State
とその実装クラス群)は個別の状態に特化した処理が実装される
- 状態毎のクラスを実行するクラス(
Context
)はchange_state()
でstate
クラスを受け取り、処理をstate
クラスに委譲する
- このようにすることで、各状態毎に実行される処理をクラスとして独立させることができる
状態を判定する条件分岐はFactoryに隠蔽する
- 状態の判定と状態に対応したクラスの生成は
Factory
クラスで行う
class State(metaclass=ABCMeta):
# 先に示したコードと同じなのでここでは省略
class StateA(State):
# 先に示したコードと同じなのでここでは省略
class StateB(State):
# 先に示したコードと同じなのでここでは省略
class StateFactory:
@staticmethod
def create(status):
if status == "status01":
return StateA()
elif status == "status02":
return StateB()
else:
raise ValueError("未定義のstatus")
if __name__ == '__main__':
c = Context()
state = StateFactory.create("status01")
c.change_state(state)
print(c.exec()) # <= わーい
state = StateFactory.create("status02")
c.change_state(state)
print(c.exec()) # <= うぇーい
4. メリット
状態依存の処理を分離できる
- 特定の状態に実行される処理を個別のクラスとして明確に分離できる
- 明確に分離できるのでクラスの責務も単一になり、実装やユニットテストが容易になる
5. デメリット
コード量が増える
- ひとつのメソッドのコード量は低下するが、全体のコード量は逆に増える
-
State
パターンを知らないエンジニアや新人エンジニアにとっては逆に修正難易度の高いコードになる可能性もある
6. 注意事項
WEBアプリ/APIでは利用することはあまりない
- WEBアプリ/APIは基本的にステートレスなので状態を表現する
State
パターンを選択する場面はほぼ無い
- DBや外部APIの状態に応じて異なる処理をすることはあるかもしれないが、その場合でも
Strategy
パターンが第一選択肢となる
- どちらかと言うとGUIアプリケーションやフロントエンドアプリケーションで活用されるパターンである
StateパターンとStrategyパターンの違い
-
Strategy
パターンは委譲クラスをコンストラクタで受け取る
-
State
パターンは委譲クラスを状態変更メソッド(Context#change_state()
)で受け取る
- 状態変更メソッドで委譲クラスを受け取ることでプログラムの実行中に動的に処理を差し替えるという動作になる
- クラスの初期化時に委譲クラスを受け取るか、実行中にインスタンスの委譲クラスを差し替えるかという点が異なる
参考資料
Appendix-1 Contextクラスに状態判定をさせるパターン
-
Context
クラスの外部で状態判定をするのではなく、Context
クラス自身に状態判定をさせる実装パターンもある
-
Context
クラスが状態変化に責任を負うという発想の場合はこちらの実装でもよいかもしれない
class Context:
def change_state(self, status):
""" stateを変更する """
self._state = StateFactory.create(status)
def exec(self):
""" stateに委譲した処理を呼び出す """
return self._state.exec()
class State(metaclass=ABCMeta):
# 先に示したコードと同じなのでここでは省略
class StateA(State):
# 先に示したコードと同じなのでここでは省略
class StateB(State):
# 先に示したコードと同じなのでここでは省略
class StateFactory:
# 先に示したコードと同じなのでここでは省略
if __name__ == '__main__':
c = Context()
c.change_state("status01")
print(c.exec()) # <= わーい
c.change_state("status02")
print(c.exec()) # <= うぇーい