Menu
Coddy logo textTech

Stateパターン

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

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

自動販売機を想像してみてください。お金を待っている状態か、支払いを受け取った状態か、商品を提供している状態かによって、異なる動作をします。

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

<?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";
    }
}

context class は current state への参照を保持し、呼び出しをそれに delegates します:

<?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

Stateパターンは、広がりすぎたswitch文をなくし、新しい状態を簡単に追加できるようにします。各状態クラスは次に遷移する状態を把握する責任を担うため、ロジックを整理された保守しやすい状態に保てます。

challenge icon

チャレンジ

簡単

State Pattern を使用してドキュメントワークフローシステムを構築しましょう。コンテンツ管理システム内のドキュメントは、draft、review、published という異なる段階を経て進みます。それぞれの段階には、許可されるアクションと、ドキュメントを次の段階へ進めようとしたときの動作に関する独自のルールがあります。

コードを4つのファイルに整理します。

  • DocumentState.php: すべてのドキュメント状態の契約を定義する DocumentState interface を Define します。2つの methods を持たせます。ドキュメントを進めようとする publish(Document $document): string と、current state name を返す getStatus(): string です。
  • States.php: DocumentState interface を含め、それを Implement する3つの state classes を Create します。
    • DraftState: publish() が呼び出されると、ドキュメントを ReviewState に遷移させ、"Document sent for review" を返します。その status は "Draft" です。
    • ReviewState: publish() が呼び出されると、ドキュメントを PublishedState に遷移させ、"Document published" を返します。その status は "Under Review" です。
    • PublishedState: publish() が呼び出されると、state を変更せずに "Document is already published" を返します。その status は "Published" です。
  • Document.php: States file を含め、context として機能する Document class を Create します。ドキュメントには次の要件があります。
    • constructor で title を accept し、DraftState で開始する
    • current state を変更するための setState(DocumentState $state): void method を have する
    • current state's publish() method に処理を delegates する publish(): string method を have する
    • current state's status を返す getStatus(): string method を have する
  • main.php: Document file を含めます。ドキュメントに対して publish() を call する回数として、1つの input を受け取ります。

    title "My Article"Document を Create します。まず initial status を Print します。次に、指定された number of times だけ publish() を call し、各 call の result を新しい line に printing します。最後に final status を Print します。

    Output format:

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

各 state class は workflow で次に来る state を把握しているため、transition logic が整理され、変更も簡単です。"Archived" のような新しい state を追加する場合は、新しい class を Create し、関連する transitions を updating するだけです。

自分で試してみよう

<?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腕試し

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

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

自分で練習してみよう: PHPオンラインコンパイラ