1. 概要

このチュートリアルでは、ステートマシンと、列挙型を使用してJavaに実装する方法について説明します。

また、各状態にインターフェイスと具象クラスを使用する場合と比較した、この実装の利点についても説明します。

2. Java列挙

Java Enum は、定数のリストを定義する特殊なタイプのクラスです。 これにより、タイプセーフな実装とより読みやすいコードが可能になります。

例として、従業員から提出された休暇申請を承認できるHRソフトウェアシステムがあるとします。 このリクエストはチームリーダーによってレビューされ、チームリーダーはそれを部門マネージャーにエスカレーションします。 部門マネージャーは、リクエストを承認する責任があります。

休暇申請の状態を保持する最も単純な列挙型は次のとおりです。

public enum LeaveRequestState {
    Submitted,
    Escalated,
    Approved
}

この列挙型の定数を参照できます。

LeaveRequestState state = LeaveRequestState.Submitted;

列挙型にはメソッドを含めることもできます。列挙型に抽象メソッドを記述できます。これにより、すべての列挙型インスタンスにこのメソッドの実装が強制されます。 これは、以下で説明するように、ステートマシンの実装にとって非常に重要です。

Java列挙型は暗黙的にクラスjava.lang.Enumを拡張するため、別のクラスを拡張することはできません。 ただし、他のクラスと同じように、インターフェイスを実装できます。

抽象メソッドを含む列挙型の例を次に示します。

public enum LeaveRequestState {
    Submitted {
        @Override
        public String responsiblePerson() {
            return "Employee";
        }
    },
    Escalated {
        @Override
        public String responsiblePerson() {
            return "Team Leader";
        }
    },
    Approved {
        @Override
        public String responsiblePerson() {
            return "Department Manager";
        }
    };

    public abstract String responsiblePerson();
}

最後の列挙型定数の最後にあるセミコロンの使用法に注意してください。 定数の後に1つ以上のメソッドがある場合は、セミコロンが必要です。

この場合、最初の例を responsiblePerson()メソッドで拡張しました。 これにより、各アクションの実行責任者がわかります。 したがって、 Escalated 状態の責任者を確認しようとすると、「チームリーダー」が表示されます。

LeaveRequestState state = LeaveRequestState.Escalated;
assertEquals("Team Leader", state.responsiblePerson());

同様に、リクエストの承認の責任者を確認すると、「部門マネージャー」が表示されます。

LeaveRequestState state = LeaveRequestState.Approved;
assertEquals("Department Manager", state.responsiblePerson());

3. ステートマシン

ステートマシン(有限ステートマシンまたは有限オートマトンとも呼ばれます)は、抽象マシンを構築するために使用される計算モデルです。 これらのマシンは、一度に1つの状態にしかなれません。各状態は、別の状態に変化するシステムのステータスです。 これらの状態変化は遷移と呼ばれます。

ダイアグラムや表記法を使用すると数学が複雑になる可能性がありますが、プログラマーにとってははるかに簡単です。

State Pattern は、GoFのよく知られた23のデザインパターンの1つです。 このパターンは、数学のモデルから概念を借用しています。 これにより、オブジェクトは、その状態に基づいて、同じオブジェクトのさまざまな動作をカプセル化できます。 状態間の遷移をプログラムし、後で別々の状態を定義することができます。

概念をよりよく説明するために、休暇要求の例を拡張してステートマシンを実装します。

4. ステートマシンとしての列挙

Javaでのステートマシンの列挙型実装に焦点を当てます。 他の実装も可能です。次のセクションでそれらを比較します。

列挙型を使用したステートマシン実装の主なポイントは、状態の明示的な設定を処理する必要がないことです。 代わりに、ある状態から次の状態に移行する方法に関するロジックを提供することができます。 さっそく飛び込みましょう:

public enum LeaveRequestState {

    Submitted {
        @Override
        public LeaveRequestState nextState() {
            return Escalated;
        }

        @Override
        public String responsiblePerson() {
            return "Employee";
        }
    },
    Escalated {
        @Override
        public LeaveRequestState nextState() {
            return Approved;
        }

        @Override
        public String responsiblePerson() {
            return "Team Leader";
        }
    },
    Approved {
        @Override
        public LeaveRequestState nextState() {
            return this;
        }

        @Override
        public String responsiblePerson() {
            return "Department Manager";
        }
    };

    public abstract LeaveRequestState nextState(); 
    public abstract String responsiblePerson();
}

この例では、ステートマシン遷移は、列挙型の抽象メソッドを使用して実装されています。 より正確には、各列挙型定数で nextState()を使用して、次の状態への遷移を指定します。 必要に応じて、 previousState()メソッドを実装することもできます。

以下は、実装を確認するためのテストです。

LeaveRequestState state = LeaveRequestState.Submitted;

state = state.nextState();
assertEquals(LeaveRequestState.Escalated, state);

state = state.nextState();
assertEquals(LeaveRequestState.Approved, state);

state = state.nextState();
assertEquals(LeaveRequestState.Approved, state);

Submitted初期状態で脱退リクエストを開始します。 次に、上記で実装した nextState()メソッドを使用して、状態遷移を検証します。

承認済みが最終状態であるため、他の遷移は発生しないことに注意してください

5. Java列挙型を使用してステートマシンを実装する利点

インターフェイスと実装クラスを備えたステートマシンの実装は、開発および保守するための大量のコードになる可能性があります。

Java列挙型は、最も単純な形式では定数のリストであるため、列挙型を使用して状態を定義できます。 また、列挙型には動作を含めることもできるため、メソッドを使用して状態間の遷移実装を提供できます。

すべてのロジックを単純な列挙型に含めることで、クリーンでわかりやすいソリューションが可能になります。

6. 結論

この記事では、ステートマシンと、それらをEnumを使用してJavaで実装する方法について説明しました。 例を挙げてテストしました。

最終的には、列挙型を使用してステートマシンを実装することの利点についても説明しました。 インターフェイスと実装ソリューションの代替として、enumはステートマシンのよりクリーンで理解しやすい実装を提供します。

いつものように、この記事で言及されているすべてのコードスニペットは、GitHubリポジトリにあります。