1. 概要

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

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

2. 状態デザインパターン

状態パターンの主なアイデアはクラスを変更せずにオブジェクトの動作を変更できるようにします。 また、それを実装することにより、コードは多くのif/elseステートメントなしでクリーンなままになります。

郵便局に送られるパッケージがあると想像してください。パッケージ自体を注文して、郵便局に配達し、最終的にクライアントが受け取ることができます。 さて、実際の状態に応じて、配信状況を印刷したいと思います。

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

さらに、各状態のすべてのロジックは、すべてのメソッドに分散されます。 さて、これはStateパターンが使用すると考えられる場所です。 Stateデザインパターンのおかげで、ロジックを専用クラスにカプセル化し、単一責任原則およびオープン/クローズド原則を適用できます。コードはよりクリーンで保守しやすくなります。

3. UML図

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

私たちのコンテキストは、動作を状態の実装に委任します。つまり、すべての着信要求は、状態の具体的な実装によって処理されます。

ロジックが分離されており、新しい状態を追加するのは簡単です。必要に応じて、別の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()、および printStatus()メソッドに注意してください。 状態は相互にリンクされ、すべての状態は、両方のメソッドに渡されたこの参照に基づいて別の状態を設定します。

クライアントは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());
    }
}

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

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

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ダイアグラムは同じであり、背後にある考え方が少し異なります。

まず、戦略パターンは、交換可能なアルゴリズムのファミリーを定義します。 一般に、これらは同じ目標を達成しますが、たとえば、並べ替えやレンダリングアルゴリズムなど、実装が異なります。

状態パターンでは、実際の状態に基づいて、動作が完全に変わる可能性があります

次、 戦略では、クライアントはそれらを明示的に使用および変更するための可能な戦略を認識している必要があります。 一方、状態パターンでは、各状態は別の状態にリンクされ、有限状態マシンのようにフローを作成します。

8. 結論

状態のデザインパターンは、プリミティブなif/elseステートメントを避けたい場合に最適です。 代わりに、ロジックを抽出してクラスを分離しコンテキストオブジェクトに動作を状態クラスに実装されているメソッドに委任させます。 さらに、1つの状態がコンテキストの状態を変更できる、状態間の遷移を活用できます。

一般に、このデザインパターンは比較的単純なアプリケーションには最適ですが、より高度なアプローチについては、Springのステートマシンチュートリアルを参照してください。

いつものように、完全なコードはGitHubプロジェクトで入手できます。