Menu
Coddy logo textTech

ステートパターン

CoddyのPHPジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 79/91。

ステートパターン(State Pattern)は、オブジェクトの内部状態が変化したときに、そのオブジェクトの振る舞いを変えることを可能にする振る舞いに関するデザインパターンです。異なる状態を処理するために複雑な条件分岐を使用する代わりに、各状態を独自のクラスにカプセル化し、現在の状態オブジェクトに振る舞いを委譲します。

自動販売機を思い浮かべてください。お金を待っている状態、支払いを受け取った状態、あるいは商品を排出している状態かによって、その動作は異なります。

各状態には、どのアクションが許可されるかという独自のルールがあります。Stateパターンは、状態ごとにクラスを作成することでこれをモデル化します。

<?php
interface OrderState {
    public function proceed(Order $order): string;
    public function getStatus(): string;
}

class PendingState implements OrderState {
    public function proceed(Order $order): string {
        $order->setState(new ProcessingState());
        return "Order moved to processing";
    }
    
    public function getStatus(): string {
        return "Pending";
    }
}

class ProcessingState implements OrderState {
    public function proceed(Order $order): string {
        $order->setState(new ShippedState());
        return "Order has been shipped";
    }
    
    public function getStatus(): string {
        return "Processing";
    }
}

class ShippedState implements OrderState {
    public function proceed(Order $order): string {
        return "Order already shipped";
    }
    
    public function getStatus(): string {
        return "Shipped";
    }
}

コンテキストクラスは、現在の状態への参照を保持し、それに対して呼び出しを委譲します:

<?php
class Order {
    private OrderState $state;
    
    public function __construct() {
        $this->state = new PendingState();
    }
    
    public function setState(OrderState $state): void {
        $this->state = $state;
    }
    
    public function proceed(): string {
        return $this->state->proceed($this);
    }
    
    public function getStatus(): string {
        return $this->state->getStatus();
    }
}

$order = new Order();
echo $order->getStatus() . "\n";
echo $order->proceed() . "\n";
echo $order->getStatus() . "\n";
echo $order->proceed() . "\n";
echo $order->getStatus();

出力:

Pending
Order moved to processing
Processing
Order has been shipped
Shipped

ステートパターンは、肥大化したswitch文を排除し、新しい状態の追加を容易にします。各状態クラスは次にどの状態が来るかを知る責任を持ち、ロジックを整理された保守しやすい状態に保ちます。

challenge icon

チャレンジ

簡単

Stateパターンを使用して、ドキュメントのワークフローシステムを構築しましょう。コンテンツ管理システムのドキュメントは、下書き(draft)、レビュー(review)、公開済み(published)といった異なるステージを経て進みます。各ステージには、許可されるアクションや、ドキュメントを進めようとしたときに何が起こるかについての独自のルールがあります。

コードを4つのファイルに分けて構成します:

  • DocumentState.php — すべてのドキュメント状態のコントラクトを確立する DocumentState インターフェースを定義します。これには2つのメソッドが必要です。ドキュメントを進めようとするための publish(Document $document): string と、現在の状態名を返すための getStatus(): string です。
  • States.php — DocumentState インターフェースをインクルードし、それを実装する3つの状態クラスを作成します:
    • DraftStatepublish() が呼び出されると、ドキュメントを ReviewState に遷移させ、"Document sent for review" を返します。そのステータスは "Draft" です。
    • ReviewStatepublish() が呼び出されると、ドキュメントを PublishedState に遷移させ、"Document published" を返します。そのステータスは "Under Review" です。
    • PublishedStatepublish() が呼び出されると、状態を変更せずに "Document is already published" を返します。そのステータスは "Published" です。
  • Document.php — States ファイルをインクルードし、コンテキストとして機能する Document クラスを作成します。ドキュメントは以下の条件を満たす必要があります:
    • コンストラクタでタイトルを受け取り、DraftState から開始する
    • 現在の状態を変更するための setState(DocumentState $state): void メソッドを持つ
    • 現在の状態の publish() メソッドに委譲する publish(): string メソッドを持つ
    • 現在の状態のステータスを返す getStatus(): string メソッドを持つ
  • main.php — Document ファイルをインクルードします。1つの入力を受け取ります:ドキュメントに対して publish() を呼び出す回数です。

    タイトルを "My Article" として Document を作成します。まず、初期ステータスを出力します。次に、指定された回数だけ publish() を呼び出し、各呼び出しの結果を新しい行に出力します。最後に、最終的なステータスを出力します。

    出力形式:

    Status: [initial status]
    [publish result 1]
    [publish result 2]
    ...
    Status: [final status]

各状態クラスはワークフローの次にどの状態が来るかを知っているため、遷移ロジックが整理され、変更が容易になります。"Archived"(アーカイブ済み)のような新しい状態を追加する場合も、新しいクラスを作成して関連する遷移を更新するだけで済みます。

チートシート

State Pattern(状態パターン)は、各状態を独自のクラスにカプセル化することで、内部状態が変化したときにオブジェクトがその振る舞いを変えることを可能にする振る舞いに関するデザインパターンです。

すべての状態クラスが実装する状態インターフェースを定義します:

<?php
interface OrderState {
    public function proceed(Order $order): string;
    public function getStatus(): string;
}

インターフェースを実装する具体的な状態クラスを作成します:

<?php
class PendingState implements OrderState {
    public function proceed(Order $order): string {
        $order->setState(new ProcessingState());
        return "Order moved to processing";
    }
    
    public function getStatus(): string {
        return "Pending";
    }
}

class ProcessingState implements OrderState {
    public function proceed(Order $order): string {
        $order->setState(new ShippedState());
        return "Order has been shipped";
    }
    
    public function getStatus(): string {
        return "Processing";
    }
}

class ShippedState implements OrderState {
    public function proceed(Order $order): string {
        return "Order already shipped";
    }
    
    public function getStatus(): string {
        return "Shipped";
    }
}

現在の状態への参照を保持し、呼び出しをその状態に委譲するコンテキストクラスを作成します:

<?php
class Order {
    private OrderState $state;
    
    public function __construct() {
        $this->state = new PendingState();
    }
    
    public function setState(OrderState $state): void {
        $this->state = $state;
    }
    
    public function proceed(): string {
        return $this->state->proceed($this);
    }
    
    public function getStatus(): string {
        return $this->state->getStatus();
    }
}

使用例:

$order = new Order();
echo $order->getStatus() . "\n";  // Pending
echo $order->proceed() . "\n";     // Order moved to processing
echo $order->getStatus() . "\n";   // Processing
echo $order->proceed() . "\n";     // Order has been shipped
echo $order->getStatus();          // Shipped

Stateパターンは、複雑なswitch文を排除し、新しい状態の追加を容易にします。各状態クラスは、次にどの状態が来るかを知る責任を持ちます。

自分で試してみよう

<?php

require_once 'Document.php';

// 入力を読み込む - publish() を呼び出す回数
$n = intval(trim(fgets(STDIN)));

// TODO: タイトルが "My Article" の Document を作成する

// TODO: 初期のステータスを次の形式で出力する: "Status: [status]"

// TODO: publish() を n 回呼び出し、各結果を新しい行に出力する

// TODO: 最終的なステータスを次の形式で出力する: "Status: [status]"

?>
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン