Menu
Coddy logo textTech

ステートパターン

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

Stateパターンは、オブジェクトの内部状態が変化したときにその振る舞いを変えることを可能にし、まるでオブジェクトのクラスが変わったかのように見せます。異なる状態を処理するために複雑な条件分岐文を使用する代わりに、各状態を個別のクラスとしてカプセル化します。

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

#include <iostream>
#include <memory>

class Document;  // Forward declaration

// State interface
class DocumentState {
public:
    virtual void publish(Document& doc) = 0;
    virtual std::string getName() const = 0;
    virtual ~DocumentState() = default;
};

// Context
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(); }
};

// Concrete States
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() アクションを異なる方法で処理し、次の状態への遷移を担当します。Document は遷移ロジックを知る必要はありません。単に現在の状態に委譲するだけです。これにより、巨大な switch 文が排除され、新しい状態の追加が簡単になります。

オブジェクトの振る舞いがその状態に大きく依存しており、オブジェクトの状態に応じて切り替わる条件文が数多く存在する場合に、Stateパターンを使用します。

challenge icon

チャレンジ

簡単

State パターンを使用して、信号機コントローラーを構築しましょう。信号機が異なる状態(Red、Yellow、Green)を循環し、各状態が信号が変わったときに何が起こるか、およびどのようなメッセージが表示されるかを決定するシステムを作成します。これは State パターンの典型的な例です。信号機の動作は、現在の状態に完全に依存します。

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

  • TrafficLightState.h: 状態インターフェースとすべての具体的な状態クラスを定義します。

    以下の内容を持つ抽象クラス TrafficLightState を作成します:

    • 次の状態への遷移を処理する純粋仮想メソッド change(TrafficLight& light)
    • 現在の色を文字列として返す純粋仮想メソッド getColor()
    • ドライバーがすべき行動(例:"Stop""Caution""Go")を返す純粋仮想メソッド getAction()
    • 仮想デストラクタ

    3 つの具体的な状態を実装します:

    • RedState — 色は "Red"、アクションは "Stop"、Green に遷移します
    • YellowState — 色は "Yellow"、アクションは "Caution"、Red に遷移します
    • GreenState — 色は "Green"、アクションは "Go"、Yellow に遷移します

    状態が TrafficLight を参照するため、TrafficLight の前方宣言が必要になります。

  • TrafficLight.h: 現在の状態を保持するコンテキストクラスを作成します。

    TrafficLight クラスは std::unique_ptr<TrafficLightState> を保持し、デフォルトで Red 状態から開始する必要があります。以下を実装してください:

    • setState(std::unique_ptr<TrafficLightState> newState) — 現在の状態を変更します
    • change() — 現在の状態の change メソッドに処理を委譲します
    • display() — 信号の状態を [Color]: [Action] の形式で出力します
  • main.cpp: 信号機が状態を循環する様子を実演します。

    1 つの入力を読み取ります:実行する状態変化の回数(整数)。

    TrafficLight を作成し、その初期状態を表示します。その後、指定された回数の変化を実行し、各変化の後に状態を表示します。

例えば、入力が 3 の場合:

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

入力が 6 の場合:

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

信号機が Red → Green → Yellow → Red という予測可能なパターンで状態を循環していることに注目してください。各状態は次にどの状態が来るかを知っており、自身の遷移を処理します。TrafficLight クラスは、何が起こるかを決定するための条件分岐ロジックを必要としません。単に現在の状態に処理を委譲するだけです。

チートシート

State パターンは、オブジェクトの内部状態が変化したときに、その振る舞いを変更できるようにします。複雑な条件分岐文を使用する代わりに、各状態を個別のクラスとしてカプセル化します。

このパターンは、次の3つのコンポーネントで構成されています:

  • State インターフェース: 状態固有の振る舞いを定義します
  • Concrete States (具体的な状態): 各状態の振る舞いを実装します
  • Context (コンテキスト): 現在の状態への参照を保持します

基本的な構造:

#include <memory>

class Context;  // 前方宣言

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

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

// Concrete State (具体的な状態)
class ConcreteState : public State {
public:
    void handleAction(Context& ctx) override {
        // アクションを処理し、次の状態へ遷移する
        ctx.setState(std::make_unique<AnotherState>());
    }
    std::string getName() const override { 
        return "ConcreteState"; 
    }
};

各状態はアクションを異なる方法で処理し、次の状態への遷移を担当します。Context は、遷移ロジックを知る必要なく、現在の状態に振る舞いを委譲します。

オブジェクトの振る舞いがその状態に大きく依存し、巨大な条件分岐文を避けたい場合に State パターンを使用します。

自分で試してみよう

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

using namespace std;

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

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

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