Stateパターンで状態遷移を管理：ワークフローエンジンの実装

## はじめに

ソフトウェア開発において、複雑な状態遷移を管理することは常に課題の一つです。特に、ワークフローエンジンのように状態遷移が頻繁に発生するシステムでは、コードの可読性と保守性を保つことが重要です。この記事では、Stateパターンを使用してこの課題に取り組む方法を紹介します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/c602bbbb-4fcf-7517-21f3-135ec74dba16.png)


## Stateパターンとは

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/0a1c1203-0653-756a-3116-699a5567519a.png)

Stateパターンは、オブジェクトの内部状態が変化したときに、そのオブジェクトの振る舞いを変化させる設計パターンです。このパターンを使用することで、状態に依存する振る舞いをカプセル化し、状態ごとに別々のクラスとして実装することが可能になります。

## なぜStateパターンを使うのか

1. **コードの整理**: 各状態の振る舞いを別々のクラスに分離することで、コードが整理され、理解しやすくなります。
2. **保守性の向上**: 新しい状態を追加する際、既存のコードに影響を与えずに新しいクラスを追加できます。
3. **状態遷移の明確化**: 状態遷移のロジックを集中管理でき、全体の流れが把握しやすくなります。
4. **単一責任の原則**: 各状態クラスは特定の状態に関する振る舞いのみを持つため、単一責任の原則に従います。

## 使い所

Stateパターンは以下のような場合に特に有効です：

- オブジェクトの振る舞いが現在の状態に依存し、状態が動的に変化する場合
- 状態遷移のロジックが複雑で、if-else文やswitch文が多用されている場合
- 状態に関連する処理が複数のメソッドに分散している場合

## メリット

1. **コードの可読性向上**: 状態ごとの振る舞いが明確に分離されるため、コードが読みやすくなります。
2. **拡張性**: 新しい状態を追加する際、既存のコードを変更せずに新しいクラスを追加するだけで済みます。
3. **状態遷移の一元管理**: 状態遷移のロジックを一箇所で管理できるため、バグの発生を減らし、デバッグも容易になります。
4. **テストの容易さ**: 各状態クラスを個別にテストできるため、ユニットテストが書きやすくなります。

## デメリット

1. **クラス数の増加**: 状態の数だけクラスを作成する必要があるため、小規模なプロジェクトでは過剰な設計になる可能性があります。
2. **複雑さの増加**: 状態遷移のロジックが複雑な場合、全体の流れを把握するのが難しくなることがあります。
3. **パフォーマンスへの影響**: 多数のオブジェクトを生成する必要があるため、メモリ使用量が増加する可能性があります。

## 実装例：シンプルなワークフローエンジン

以下に、Stateパターンを使用したシンプルなワークフローエンジンの実装例を示します。この例では、ドキュメントの承認プロセスを模擬しています。

```mermaid
classDiagram
    class Workflow {
        -State state
        +change_state(State)
        +submit()
        +review()
        +approve()
        +reject()
    }
    
    class State {
        <<interface>>
        +submit(Workflow)
        +review(Workflow)
        +approve(Workflow)
        +reject(Workflow)
    }
    
    class DraftState {
        +submit(Workflow)
        +review(Workflow)
        +approve(Workflow)
        +reject(Workflow)
    }
    
    class SubmittedState {
        +submit(Workflow)
        +review(Workflow)
        +approve(Workflow)
        +reject(Workflow)
    }
    
    class ReviewState {
        +submit(Workflow)
        +review(Workflow)
        +approve(Workflow)
        +reject(Workflow)
    }
    
    class ApprovedState {
        +submit(Workflow)
        +review(Workflow)
        +approve(Workflow)
        +reject(Workflow)
    }
    
    Workflow o-- State : has
    State <|.. DraftState : implements
    State <|.. SubmittedState : implements
    State <|.. ReviewState : implements
    State <|.. ApprovedState : implements
```

```mermaid
sequenceDiagram
    actor Client
    participant Workflow
    participant DraftState
    participant SubmittedState
    participant ReviewState
    participant ApprovedState

    Client->>+Workflow: 新規ワークフロー作成
    Workflow->>DraftState: 初期状態設定
    Workflow-->>-Client: ワークフロー作成完了

    Client->>+Workflow: submit()
    Workflow->>+DraftState: submit()
    DraftState->>Workflow: change_state(SubmittedState)
    DraftState-->>-Workflow: 完了
    Workflow-->>-Client: 提出完了

    Client->>+Workflow: review()
    Workflow->>+SubmittedState: review()
    SubmittedState->>Workflow: change_state(ReviewState)
    SubmittedState-->>-Workflow: 完了
    Workflow-->>-Client: レビュー開始

    Client->>+Workflow: approve()
    Workflow->>+ReviewState: approve()
    ReviewState->>Workflow: change_state(ApprovedState)
    ReviewState-->>-Workflow: 完了
    Workflow-->>-Client: 承認完了

    Client->>+Workflow: submit()
    Workflow->>+ApprovedState: submit()
    ApprovedState-->>-Workflow: 既に承認済みのメッセージ
    Workflow-->>-Client: 操作不可のメッセージ
```

```python
from abc import ABC, abstractmethod

# Stateインターフェース
class State(ABC):
    @abstractmethod
    def submit(self, workflow):
        pass

    @abstractmethod
    def review(self, workflow):
        pass

    @abstractmethod
    def approve(self, workflow):
        pass

    @abstractmethod
    def reject(self, workflow):
        pass

# 具体的な状態クラス
class DraftState(State):
    def submit(self, workflow):
        print("文書を提出しました。レビュー待ちです。")
        workflow.change_state(SubmittedState())  # 状態をSubmittedStateに変更

    def review(self, workflow):
        print("ドラフト状態では、レビューできません。")

    def approve(self, workflow):
        print("ドラフト状態では、承認できません。")

    def reject(self, workflow):
        print("ドラフト状態では、却下できません。")

class SubmittedState(State):
    def submit(self, workflow):
        print("すでに提出済みです。")

    def review(self, workflow):
        print("レビューを開始しました。")
        workflow.change_state(ReviewState())  # 状態をReviewStateに変更

    def approve(self, workflow):
        print("レビュー前の承認はできません。")

    def reject(self, workflow):
        print("レビュー前の却下はできません。")

class ReviewState(State):
    def submit(self, workflow):
        print("すでにレビュー中です。")

    def review(self, workflow):
        print("レビューは既に進行中です。")

    def approve(self, workflow):
        print("文書が承認されました。")
        workflow.change_state(ApprovedState())  # 状態をApprovedStateに変更

    def reject(self, workflow):
        print("文書が却下されました。修正してください。")
        workflow.change_state(DraftState())  # 状態をDraftStateに戻す

class ApprovedState(State):
    def submit(self, workflow):
        print("すでに承認済みです。")

    def review(self, workflow):
        print("承認済みの文書は再レビューできません。")

    def approve(self, workflow):
        print("すでに承認済みです。")

    def reject(self, workflow):
        print("承認済みの文書は却下できません。")

# コンテキストクラス
class Workflow:
    def __init__(self):
        self.state = DraftState()  # 初期状態はDraftState

    def change_state(self, state):
        self.state = state  # 状態を変更するメソッド

    def submit(self):
        self.state.submit(self)

    def review(self):
        self.state.review(self)

    def approve(self):
        self.state.approve(self)

    def reject(self):
        self.state.reject(self)

# 使用例
if __name__ == "__main__":
    workflow = Workflow()

    workflow.submit()   # 文書を提出しました。レビュー待ちです。
    workflow.review()   # レビューを開始しました。
    workflow.reject()   # 文書が却下されました。修正してください。
    workflow.submit()   # 文書を提出しました。レビュー待ちです。
    workflow.review()   # レビューを開始しました。
    workflow.approve()  # 文書が承認されました。
    workflow.submit()   # すでに承認済みです。
```

## 実装の解説

1. `State`抽象クラスで、各状態で可能な操作を定義しています。
2. 具体的な状態クラス（`DraftState`、`SubmittedState`、`ReviewState`、`ApprovedState`）で、各操作の具体的な振る舞いを実装しています。
3. `Workflow`クラスが状態を保持し、操作に応じて適切な状態オブジェクトのメソッドを呼び出します。
4. 状態遷移は各状態クラス内で`workflow.change_state()`メソッドを呼び出すことで行われます。

## 状態クラスの命名について

この例では、`DraftState`や`ApprovedState`など、具体的な業務用語を使用していますが、これらの名前は実際の業務に合わせて自由にカスタマイズ可能です。例えば、より一般的な`InitialState`や`FinalState`といった名前を使用することも可能です。重要なのは、状態の役割が明確に伝わる名前を選択することです。

## 具体的なユースケース

1. **ドキュメント提出後の再レビュー**: 例えば、レビュー中に追加の情報が必要になった場合、`ReviewState`に`requestAdditionalInfo`メソッドを追加し、`SubmittedState`に戻す処理を実装できます。

2. **承認後の別状態への遷移**: 承認後に公開プロセスが必要な場合、`ApprovedState`の後に`PublishingState`を追加し、`approve`メソッド内で`PublishingState`に遷移する処理を実装できます。

## 状態遷移のテスト戦略

Stateパターンを使用したシステムのテストでは、以下のアプローチが効果的です：

1. **単体テスト**: 各状態クラスのメソッドが期待通りの動作をするかテストします。
2. **統合テスト**: `Workflow`クラスを使用して、異なる状態遷移シナリオをテストします。
3. **エッジケーステスト**: 無効な操作（例：ドラフト状態での承認）が適切に処理されることを確認します。
4. **全遷移パスのテスト**: すべての可能な状態遷移パスを網羅するテストケースを作成します。

テスト時は、モックオブジェクトを使用して外部依存を排除し、状態遷移ロジックに焦点を当てたテストを行うことが重要です。

## 実際の使用例

Stateパターンは様々な分野で活用されています。

### 1. 注文管理システム

**概要**: 大手ECサイトの注文管理システムにStateパターンを適用。

**成功点**:
- 複雑な注文状態（未払い、支払い済み、発送準備中、発送済み、配達完了、返品処理中など）を効率的に管理。
- 新しい状態（例：キャンセル待ち）の追加が容易になり、システムの拡張性が向上。
- 各状態に対する操作（支払い、発送、キャンセルなど）の制御が明確になり、不正な操作を防止。

**課題と解決策**:
- 状態クラスが増えすぎて管理が困難になる懸念があった。 → 関連する状態をグループ化し、階層構造を導入することで管理を簡素化。
- パフォーマンスへの影響が心配された。 → オブジェクトプールを使用してインスタンス生成のオーバーヘッドを軽減。

### 2. ドキュメント承認ワークフロー

**概要**: 大企業の内部文書管理システムにStateパターンを採用。

**成功点**:
- 複数の承認者が関与する複雑なワークフローを柔軟に表現。
- 部門ごとに異なる承認フローを容易に実装。
- 監査のために各状態変更の履歴を容易に追跡可能に。

**課題と解決策**:
- 状態遷移の全体像が把握しづらくなった。 → 状態遷移図を自動生成するツールを開発し、ドキュメントに組み込む。
- テストケースの作成と保守が複雑化。 → 状態遷移をDSLで記述し、テストケースを自動生成するフレームワークを開発。

### 3. ゲーム開発におけるキャラクター制御

**概要**: RPGゲームのキャラクター制御システムにStateパターンを適用。

**成功点**:
- キャラクターの状態（通常、戦闘中、回復中、気絶など）に応じた振る舞いを明確に分離。
- 新しい状態や特殊能力の追加が容易になり、ゲームの拡張性が向上。
- デバッグが容易になり、バグの早期発見と修正に貢献。

**課題と解決策**:
- 状態間の遷移ルールが複雑化し、予期せぬ遷移が発生。 → 状態遷移ルールを外部のルールエンジンで管理し、動的に制御できるように改良。
- メモリ使用量の増大。 → Flyweightパターンを併用し、共通の状態オブジェクトを共有することでメモリ使用を最適化。

## まとめ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/411815ee-1baf-5ff0-24c7-c26e6421244f.png)

これらの事例から、Stateパターンが複雑な状態管理を必要とするシステムで非常に有効であることがわかります。特に、以下の点で大きな利点があります：

- システムの拡張性と保守性の向上
- 複雑な状態遷移ロジックの整理と可視化
- 新しい状態や振る舞いの追加が容易

一方で、状態数の増加に伴う複雑さの管理や、パフォーマンスへの影響には注意が必要です。これらの課題に対しては、適切な設計手法（階層構造の導入、オブジェクトプールの使用など）や補助ツールの開発が効果的です。

Stateパターンの適用を検討する際は、システムの要件や規模、将来の拡張性などを考慮し、このパターンがもたらす利点と課題を十分に評価することが重要です。適切に適用することで、複雑な状態遷移を持つシステムの品質と開発効率を大幅に向上させることができるでしょう。

## 最後に

Stateパターンは、複雑な状態管理を必要とするシステムにおいて強力なツールとなります。しかし、すべての状況に適しているわけではありません。小規模なプロジェクトや、状態遷移が比較的単純なシステムでは、単純なif-else文やswitch文で十分な場合もあります。

重要なのは、プロジェクトの要件をよく理解し、Stateパターンの適用がもたらす利点と課題を慎重に検討することです。適切に使用すれば、Stateパターンは保守性、拡張性、可読性に優れたコードを生み出す強力な武器となるでしょう。

実際の開発では、ここで紹介した基本的な実装をベースに、プロジェクト固有の要件に合わせてカスタマイズしていくことになります。状態遷移のログ記録、並行処理への対応、永続化の仕組みなど、実用的なシステムを構築する上では追加の考慮が必要になるでしょう。

Stateパターンの理解を深め、実際のプロジェクトでの適用を通じて、より洗練された状態管理の実現を目指してください。