Javaにおける状態設計パターン

Javaの状態設計パターン

1. 概要

このチュートリアルでは、動作GoFデザインパターンの1つであるStateパターンを紹介します。

最初に、その目的の概要を示し、解決しようとする問題について説明します。 次に、州のUML図と実際の例の実装を見ていきます。

2. 状態設計パターン

状態パターンの主な考え方は、allow the object for changing its behavior without changing its class. です。また、これを実装することで、多くのif / elseステートメントがなくてもコードがクリーンに保たれるはずです。

郵便局に送られた荷物があるとします。荷物自体を注文してから郵便局に配達し、最終的に顧客が受け取ることができます。 次に、実際の状態に応じて、配信ステータスを印刷します。

最も単純なアプローチは、ブールフラグをいくつか追加し、クラス内の各メソッド内に単純なif / elseステートメントを適用することです。 単純なシナリオでは、それほど複雑にはなりません。 ただし、処理する状態が増えると、コードが複雑になり、汚染される可能性があります。その結果、if / elseステートメントがさらに増えることになります。

また、各状態のすべてのロジックは、すべてのメソッドに分散されます。 現在、これはStateパターンが使用されると考えられる場所です。 Stateデザインパターンのおかげで、ロジックを専用クラスにカプセル化し、Single Responsibility PrincipleOpen/Closed Principle, haveのよりクリーンで保守性の高いコードを適用できます。

3. UML図

UML diagram of state design pattern

UMLダイアグラムでは、Contextクラスに関連するStateがあり、プログラムの実行中に変更されることがわかります。

コンテキストはdelegate the behavior to the state implementation.になります。言い換えると、すべての着信要求は、状態の具体的な実装によって処理されます。

ロジックが分離されており、新しい状態を追加するのは簡単です。必要に応じて、別のState実装を追加することになります。

4. 実装

アプリケーションを設計しましょう。 すでに述べたように、パッケージは注文、配信、受信できるため、3つの状態とコンテキストクラスがあります。

まず、コンテキストを定義しましょう。これはPackageクラスになります。

public class Package {

    private PackageState state = new OrderedState();

    // getter, setter

    public void previousState() {
        state.prev(this);
    }

    public void nextState() {
        state.next(this);
    }

    public void printStatus() {
        state.printStatus();
    }
}

ご覧のとおり、状態を管理するための参照が含まれています。ジョブを状態オブジェクトに委任するpreviousState(), nextState() and printStatus()メソッドに注意してください。 状態は相互にリンクされ、every state will set another one based on this referenceが両方のメソッドに渡されます。

クライアントはPackageクラスと対話しますが、状態の設定に対処する必要はありません。クライアントが行う必要があるのは、次または前の状態に移動することだけです。

次に、PackageStateを作成します。これには、次のシグネチャを持つ3つのメソッドがあります。

public interface PackageState {

    void next(Package pkg);
    void prev(Package pkg);
    void printStatus();
}

このインターフェイスは、各具象状態クラスによって実装されます。

最初の具体的な状態はOrderedStateになります。

public class OrderedState implements PackageState {

    @Override
    public void next(Package pkg) {
        pkg.setState(new DeliveredState());
    }

    @Override
    public void prev(Package pkg) {
        System.out.println("The package is in its root state.");
    }

    @Override
    public void printStatus() {
        System.out.println("Package ordered, not delivered to the office yet.");
    }
}

ここでは、パッケージの注文後に発生する次の状態を示します。 順序付けられた状態はルート状態であり、明示的にマークします。 両方の方法で、状態間の遷移がどのように処理されるかを確認できます。

DeliveredStateクラスを見てみましょう。

public class DeliveredState implements PackageState {

    @Override
    public void next(Package pkg) {
        pkg.setState(new ReceivedState());
    }

    @Override
    public void prev(Package pkg) {
        pkg.setState(new OrderedState());
    }

    @Override
    public void printStatus() {
        System.out.println("Package delivered to post office, not received yet.");
    }
}

繰り返しますが、状態間のリンクを確認します。 パッケージの状態が注文済みから配信済みに変更され、printStatus()のメッセージも変更されます。

最後のステータスはReceivedStateです。

public class ReceivedState implements PackageState {

    @Override
    public void next(Package pkg) {
        System.out.println("This package is already received by a client.");
    }

    @Override
    public void prev(Package pkg) {
        pkg.setState(new DeliveredState());
    }
}

これが最後の状態に到達する場所であり、前の状態にのみロールバックできます。

1つの州が他の州を知っているので、すでにいくらかの見返りがあることがわかります。 私たちはそれらを緊密に結合させています。

5. テスト

実装がどのように動作するかを見てみましょう。 まず、セットアップの移行が期待どおりに機能するかどうかを確認しましょう。

@Test
public void givenNewPackage_whenPackageReceived_thenStateReceived() {
    Package pkg = new Package();

    assertThat(pkg.getState(), instanceOf(OrderedState.class));
    pkg.nextState();

    assertThat(pkg.getState(), instanceOf(DeliveredState.class));
    pkg.nextState();

    assertThat(pkg.getState(), instanceOf(ReceivedState.class));
}

次に、パッケージがその状態で戻ることができるかどうかをすばやく確認します。

@Test
public void givenDeliveredPackage_whenPrevState_thenStateOrdered() {
    Package pkg = new Package();
    pkg.setState(new DeliveredState());
    pkg.previousState();

    assertThat(pkg.getState(), instanceOf(OrderedState.class));
}

その後、状態の変更を確認し、printStatus()メソッドの実装が実行時に実装をどのように変更するかを見てみましょう。

public class StateDemo {

    public static void main(String[] args) {

        Package pkg = new Package();
        pkg.printStatus();

        pkg.nextState();
        pkg.printStatus();

        pkg.nextState();
        pkg.printStatus();

        pkg.nextState();
        pkg.printStatus();
    }
}

これにより、次の出力が得られます。

Package ordered, not delivered to the office yet.
Package delivered to post office, not received yet.
Package was received by client.
This package is already received by a client.
Package was received by client.

コンテキストの状態を変更しているため、動作は変更されていましたが、クラスは同じままです。 使用するAPIと同様に。

また、状態間の遷移が発生し、クラスは状態を変更し、結果として動作を変更しました。

6. 欠点

状態パターンの欠点は、状態間の遷移を実装する際の見返りです。 これにより、状態がハードコーディングされますが、これは一般に悪い習慣です。

しかし、私たちのニーズと要件に応じて、それは問題になる場合とそうでない場合があります。

7. 状態対 戦略パターン

両方の設計パターンは非常に似ていますが、それらのUMLダイアグラムは同じであり、それらの背後にある考え方はわずかに異なります。

まず、strategy pattern defines a family of interchangeable algorithms。 一般に、それらは同じ目標を達成しますが、たとえば、ソートまたはレンダリングアルゴリズムなどの異なる実装を使用します。

In state pattern, the behavior might change completely、実際の状態に基づく。

次に、in strategy, the client has to be aware of the possible strategies to use and change them explicitly.状態パターンでは、各状態が別の状態にリンクされ、有限状態マシンのようにフローが作成されます。

8. 結論

avoid primitive if/else statementsにしたい場合、状態デザインパターンは素晴らしいです。 代わりに、extract the logic to separate classesを実行し、context object delegate the behaviorを状態クラスに実装されたメソッドに割り当てます。 また、1つの状態がコンテキストの状態を変更できる状態間の遷移を活用できます。

一般に、このデザインパターンは比較的単純なアプリケーションには最適ですが、より高度なアプローチについては、Spring’s State Machine tutorialを確認できます。

いつものように、完全なコードはthe GitHub projectで入手できます。