Menu
Coddy logo textTech

Stateパターン

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

State パターンを使用すると、オブジェクトの内部状態が変化したときにその振る舞いを変更でき、あたかもオブジェクトの class が変わったかのように見せることができます。さまざまな状態を処理するために複雑な条件文を使用する代わりに、それぞれの状態を個別の class としてカプセル化します。

このパターンは、現在の状態への参照を保持する Context、状態固有の振る舞いを定義する State interface、および各状態の振る舞いを実装する Concrete States で構成されます:

#include <iostream>
#include <memory>

class Document;  // 前方宣言

// State インターフェース
class DocumentState {
public:
    virtual void publish(Document& doc) = 0;
    virtual std::string getName() const = 0;
    virtual ~DocumentState() = default;
};

// コンテキスト
class Document {
    std::unique_ptr<DocumentState> state;
public:
    Document();
    void setState(std::unique_ptr<DocumentState> newState) {
        state = std::move(newState);
    }
    void publish() { state->publish(*this); }
    std::string getStateName() const { return state->getName(); }
};

// 具象状態
class Draft : public DocumentState {
public:
    void publish(Document& doc) override;
    std::string getName() const override { return "Draft"; }
};

class Review : public DocumentState {
public:
    void publish(Document& doc) override;
    std::string getName() const override { return "Review"; }
};

class Published : public DocumentState {
public:
    void publish(Document& doc) override {
        std::cout << "Already published\n";
    }
    std::string getName() const override { return "Published"; }
};

void Draft::publish(Document& doc) {
    std::cout << "Moving to review\n";
    doc.setState(std::make_unique<Review>());
}

void Review::publish(Document& doc) {
    std::cout << "Publishing document\n";
    doc.setState(std::make_unique<Published>());
}

Document::Document() : state(std::make_unique<Draft>()) {}

各状態は publish() action をそれぞれ異なる方法で処理し、次の状態への遷移を担います。Document は遷移ロジックを知る必要がなく、単に current state に委譲します。これにより、大規模な switch 文が不要になり、新しい状態を簡単に追加できるようになります。

オブジェクトの動作がその状態に大きく依存し、オブジェクトの状態に応じて切り替わる条件文が多数ある場合は、Stateを使用します。

challenge icon

チャレンジ

簡単

Stateパターンを使用して、Traffic Light Controllerを構築しましょう。信号機が異なる状態(Red、Yellow、Green)を順番に切り替え、各状態が信号の変更時に何が起こるか、またどのメッセージを表示するかを決定するシステムを作成します。これはStateパターンの典型的な例です。信号機の動作は、現在の状態に完全に依存します。

コードを3つのファイルに分けて整理します。

  • TrafficLightState.h:state interfaceとすべての具体的なstate classを定義します。

    abstractなTrafficLightState classを作成し、次の要素を含めます。

    • 次の状態への遷移を処理する純粋仮想change(TrafficLight& light) method
    • 現在の色を文字列として返す純粋仮想getColor() method
    • 運転者が何をすべきかを返す純粋仮想getAction() method(例:"Stop"、"Caution"、"Go")
    • 仮想destructor

    3つの具体的なstateをImplementします。

    • RedState:色は"Red"、actionは"Stop"、Greenに変化します
    • YellowState:色は"Yellow"、actionは"Caution"、Redに変化します
    • GreenState:色は"Green"、actionは"Go"、Yellowに変化します

    stateがTrafficLightを参照するため、TrafficLightのForward declarationが必要です。

  • TrafficLight.h:current stateを保持するContext classを作成します。

    TrafficLight classはstd::unique_ptr<TrafficLightState>を保持し、defaultではRed stateで開始する必要があります。次をImplementします。

    • setState(std::unique_ptr<TrafficLightState> newState):current stateを変更します
    • change():current stateのchange methodにdelegatesします
    • display():信号機の状態を[Color]: [Action]のformatで出力します
  • main.cpp:信号機がstateを順番に切り替える様子を示します。

    1つの入力、つまり実行するstate changesの回数(整数)を読み取ります。

    TrafficLightをCreateし、initial stateを表示します。その後、指定された回数だけchangeを実行し、各changeの後にstateを表示します。

たとえば、入力が3の場合:

Red: Stop
Green: Go
Yellow: Caution
Red: Stop

入力が6の場合:

Red: Stop
Green: Go
Yellow: Caution
Red: Stop
Green: Go
Yellow: Caution
Red: Stop

信号機が予測可能なパターンでstateを切り替えていることに注目してください:Red → Green → Yellow → Red。各stateは次に来るstateを把握し、それ自身の遷移を処理します。TrafficLight classは、何が起こるかを決定するためのconditional logicを必要としません。単に、現在のstateにdelegatesします。

自分で試してみよう

#include <iostream>
#include "TrafficLight.h"

using namespace std;

int main() {
    // 状態変化の数を読み取る
    int numChanges;
    cin >> numChanges;
    
    // TODO: TrafficLightオブジェクトを作成する
    
    // TODO: 初期状態を表示する
    
    // TODO: 指定された回数の変更を実行する
    // 各変更の後、現在の状態を表示する
    
    return 0;
}
quiz icon腕試し

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

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

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