ステートパターン
CoddyのJavaジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 74/87。
ステートパターン(State Pattern)は、オブジェクトの内部状態が変化したときに、その振る舞いを変更できるようにする振る舞いに関するデザインパターンです。オブジェクトはあたかもクラスが変わったかのように見えますが、実際には振る舞いを異なる状態オブジェクトに委譲しています。自動販売機を例に考えてみましょう。ボタンを押したときの反応は、待機中か、お金が投入されたか、あるいは商品を排出中かによって異なります。
異なる状態を処理するために複雑な条件分岐文を使用する代わりに、各状態を独自のクラスにカプセル化します。このパターンには、現在の状態への参照を保持する Context と、各状態の振る舞いを定義する State クラスが含まれます:
interface State {
void handle(Context context);
}
class Context {
private State state;
public void setState(State state) {
this.state = state;
}
public void request() {
state.handle(this);
}
}各具象状態は振る舞いを実装し、他の状態への遷移をトリガーすることができます:
class IdleState implements State {
public void handle(Context context) {
System.out.println("Idle: Insert coin to start");
context.setState(new ActiveState());
}
}
class ActiveState implements State {
public void handle(Context context) {
System.out.println("Active: Processing...");
context.setState(new IdleState());
}
}コンテキストはリクエストを現在の状態に委譲し、状態が変化するにつれて振る舞いも自動的に変化します:
Context context = new Context();
context.setState(new IdleState());
context.request(); // 待機中: コインを入れて開始してください
context.request(); // 実行中: 処理中...ステートパターンは、オブジェクトの振る舞いがその状態に大きく依存し、実行時に変更される必要がある場合に最適です。これにより、巨大な条件分岐ブロックが排除され、新しい状態の追加が簡単になります。単にStateインターフェースを実装する新しいクラスを作成するだけです。
チャレンジ
簡単Stateパターンを使用して、ドキュメントのワークフローシステムを構築しましょう!ドキュメントが「Draft(下書き)」、「Review(レビュー)」、「Published(公開済み)」という異なるステージをどのように移動するかをモデル化するシステムを作成します。各状態(State)によって、実行可能なアクションや、編集・承認を試みた際のドキュメントの振る舞いが決定されます。
コードは以下の4つのファイルに分けて構成します:
DocumentState.java: すべての状態が実装するDocumentStateインターフェースを定義します。このインターフェースでは、ドキュメントの編集を試みるedit(Document doc)と、ドキュメントを承認して次のステージへ進めるapprove(Document doc)の2つのメソッドを宣言する必要があります。States.java:DocumentStateを実装する3つの具体的な状態クラスを作成します:DraftState-editが呼び出されたとき、Editing draft...と出力します。approveが呼び出されたとき、Draft approved. Moving to review.と出力し、ドキュメントをReviewStateに遷移させます。ReviewState-editが呼び出されたとき、Cannot edit during review.と出力します(状態遷移はなし)。approveが呼び出されたとき、Review complete. Publishing document.と出力し、PublishedStateに遷移させます。PublishedState-editが呼び出されたとき、Cannot edit published document.と出力します。approveが呼び出されたとき、Document already published.と出力します。どちらのアクションでも状態は変化しません。Document.java: 現在の状態を保持するコンテキストクラスを作成します。Documentクラスには以下を含める必要があります:現在の
DocumentStateを保持するプライベートフィールド。コンストラクタでDraftStateに初期化します。現在の状態を変更する
setState(DocumentState state)メソッド。現在の状態の
editメソッドに処理を委譲するedit()メソッド。現在の状態の
approveメソッドに処理を委譲するapprove()メソッド。現在の状態の単純なクラス名を返す
getStatus()メソッド(getClass().getSimpleName()を使用してください)。Main.java: 構築した状態システムの動作を実演します!1つの入力を受け取ります。それはeditまたはapproveのいずれかのアクションをカンマで区切った文字列です(例:edit,approve,edit,approve)。Documentを作成し、その初期ステータスをStatus: [state]の形式で出力します。次に、各アクションを順番に処理し、ドキュメントの適切なメソッドを呼び出します。各アクションの後、現在のステータスを出力してください。
入力として、カンマ区切りのアクション文字列を1つ受け取ります。
例えば、入力が edit,approve,approve の場合、出力は以下のようになります:
Status: DraftState
Editing draft...
Status: DraftState
Draft approved. Moving to review.
Status: ReviewState
Review complete. Publishing document.
Status: PublishedStateドキュメントの現在の状態に応じて、同じ approve() 呼び出しが異なる結果をもたらすことに注目してください。これがStateパターンの本質です!ドキュメントは条件分岐を使用して自身の状態をチェックするのではなく、現在の状態オブジェクトに振る舞いを委譲します。状態オブジェクトはアクションを適切に処理し、必要に応じて遷移をトリガーします。
チートシート
Stateパターンは、オブジェクトの内部状態が変化したときに、複雑な条件分岐文を使用する代わりに、振る舞いを異なる状態オブジェクトに委譲することで、そのオブジェクトの振る舞いを変えることができる振る舞いに関するデザインパターンです。
このパターンは、現在の状態への参照を保持するContextと、各状態の振る舞いを定義するStateクラスで構成されます:
interface State {
void handle(Context context);
}
class Context {
private State state;
public void setState(State state) {
this.state = state;
}
public void request() {
state.handle(this);
}
}各具体的な状態(Concrete State)は振る舞いを実装し、他の状態への遷移をトリガーすることができます:
class IdleState implements State {
public void handle(Context context) {
System.out.println("Idle: Insert coin to start");
context.setState(new ActiveState());
}
}
class ActiveState implements State {
public void handle(Context context) {
System.out.println("Active: Processing...");
context.setState(new IdleState());
}
}Contextはリクエストを現在の状態に委譲し、状態が変化するにつれて振る舞いも自動的に変化します:
Context context = new Context();
context.setState(new IdleState());
context.request(); // Idle: Insert coin to start
context.request(); // Active: Processing...Stateパターンは、オブジェクトの振る舞いがその状態に大きく依存し、実行時に変更する必要がある場合に最適です。これにより、大規模な条件分岐ブロックが排除され、新しい状態の追加が容易になります。
自分で試してみよう
import java.util.Scanner;
class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String input = scanner.nextLine();
// TODO: 新しい Document を作成する
// TODO: 初期ステータスを "Status: [state]" の形式で出力する
// TODO: 入力をカンマで分割して個々のアクションを取得する
// TODO: 各アクションをループで処理する
// - アクションが "edit" の場合、document に対して edit() を呼び出す
// - アクションが "approve" の場合、document に対して approve() を呼び出す
// - 各アクションの後、現在のステータスを出力する
}
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
4継承
継承の基本 (extends)super キーワードメソッドのオーバーライド (@Override)コンストラクタチェーンObject クラス単一継承と多段階継承なぜ多重継承ができないのかまとめ:従業員の階層構造