ステートパターン
CoddyのC#ジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 60/70。
State パターンは、オブジェクトの内部状態が変化したときに、そのオブジェクトが振る舞いを変えることを可能にする振る舞いに関するパターンです。異なる状態を処理するために複雑な条件分岐文を使用する代わりに、各状態を独自のクラスにカプセル化し、現在の状態オブジェクトに振る舞いを委譲します。
このパターンは、状態固有の振る舞いを定義する状態インターフェース、その振る舞いを実装する具体的な状態クラス、そして現在の状態への参照を保持するコンテキストクラスという3つのコンポーネントで構成されています:
public interface IDocumentState
{
void Publish(Document doc);
}
public class DraftState : IDocumentState
{
public void Publish(Document doc)
{
Console.WriteLine("Moving to moderation");
doc.SetState(new ModerationState());
}
}
public class ModerationState : IDocumentState
{
public void Publish(Document doc)
{
Console.WriteLine("Publishing document");
doc.SetState(new PublishedState());
}
}
public class PublishedState : IDocumentState
{
public void Publish(Document doc)
{
Console.WriteLine("Already published");
}
}コンテキストは現在の状態を保持し、アクションをその状態に委譲します。状態オブジェクトは、コンテキストに対して SetState() を呼び出すことで遷移をトリガーできます:
public class Document
{
private IDocumentState _state = new DraftState();
public void SetState(IDocumentState state) => _state = state;
public void Publish() => _state.Publish(this);
}
var doc = new Document();
doc.Publish(); // モデレーションへ移動
doc.Publish(); // ドキュメントを公開
doc.Publish(); // 既に公開済みStateパターンは、現在の状態をチェックする肥大化した if-else 文や switch 文を排除します。各状態クラスは自身の振る舞いのみを処理するため、コードの保守が容易になり、新しい状態の追加による拡張も簡単になります。
チャレンジ
簡単State パターンを使用して信号機システムを構築しましょう。信号機は異なる状態(赤、黄、青)を循環し、各状態が信号が変わったときに何が起こるかを決定します。複雑な条件分岐を使用する代わりに、各信号の状態を独自のクラスにカプセル化します。
コードは以下の3つのファイルに分けて構成します:
TrafficLightState.cs:Traffic名前空間内に、Change(TrafficLight light)メソッドを持つITrafficLightStateインターフェースを定義します。次に、3つの具体的な状態クラスを作成します:RedState-Changeが呼び出されると、Red - Stop! Changing to Green...と出力し、信号をGreenStateに遷移させます。GreenState-Green - Go! Changing to Yellow...と出力し、YellowStateに遷移させます。YellowState-Yellow - Caution! Changing to Red...と出力し、RedStateに遷移させます。
TrafficLight.cs: 同じ名前空間にTrafficLightクラスを作成します。これは現在の状態を保持するコンテキストクラスです。デフォルトでRedStateから開始し、以下のものを持ちます:- 状態オブジェクトが遷移をトリガーするために使用する
SetState(ITrafficLightState state)メソッド。 - 現在の状態の
Changeメソッドに処理を委譲するChange()メソッド。
- 状態オブジェクトが遷移をトリガーするために使用する
Program.cs:TrafficLightを作成し、入力に基づいて状態変化を循環させることで、すべてをまとめます。
以下の1つの入力を受け取ります:
- 信号機を切り替える回数(整数)
TrafficLight(赤から開始)を作成し、指定された回数だけ Change() を呼び出します。
例えば、入力が 3 の場合、出力は以下のようになります:
Red - Stop! Changing to Green...
Green - Go! Changing to Yellow...
Yellow - Caution! Changing to Red...入力が 5 の場合、信号は再び循環します:
Red - Stop! Changing to Green...
Green - Go! Changing to Yellow...
Yellow - Caution! Changing to Red...
Red - Stop! Changing to Green...
Green - Go! Changing to Yellow...TrafficLight クラスには、各色の意味や次に何が来るかについてのロジックが含まれていないことに注目してください。それらの知識はすべて個別の状態クラスにあります。各状態が自身の振る舞いと、次の状態への遷移のトリガーに責任を持っています!
チートシート
Stateパターンは、オブジェクトの内部状態が変化したときにその振る舞いを変更できるようにする振る舞いに関するパターンです。複雑な条件分岐文を使用する代わりに、各状態を独自のクラスにカプセル化し、現在の状態オブジェクトに振る舞いを委譲します。
このパターンは3つのコンポーネントで構成されています:
- Stateインターフェース:状態固有の振る舞いを定義します
- 具体的な状態クラス:各状態の振る舞いを実装します
- Contextクラス:現在の状態への参照を保持します
実装例:
// Stateインターフェース
public interface IDocumentState
{
void Publish(Document doc);
}
// 具体的な状態クラス
public class DraftState : IDocumentState
{
public void Publish(Document doc)
{
Console.WriteLine("Moving to moderation");
doc.SetState(new ModerationState());
}
}
public class ModerationState : IDocumentState
{
public void Publish(Document doc)
{
Console.WriteLine("Publishing document");
doc.SetState(new PublishedState());
}
}
public class PublishedState : IDocumentState
{
public void Publish(Document doc)
{
Console.WriteLine("Already published");
}
}
// Contextクラス
public class Document
{
private IDocumentState _state = new DraftState();
public void SetState(IDocumentState state) => _state = state;
public void Publish() => _state.Publish(this);
}
// 使用例
var doc = new Document();
doc.Publish(); // モデレーションへ移動
doc.Publish(); // ドキュメントを公開
doc.Publish(); // 公開済みContextは現在の状態を保持し、アクションをその状態に委譲します。状態オブジェクトは、ContextのSetState()を呼び出すことで遷移をトリガーできます。これにより、肥大化したif-else文やswitch文が排除され、コードの保守や新しい状態の追加が容易になります。
自分で試してみよう
using System;
using Traffic;
class Program
{
public static void Main(String[] args)
{
// 信号機を切り替える回数を読み込む
int n = Convert.ToInt32(Console.ReadLine());
// TODO: TrafficLight インスタンスを作成する(デフォルトで RedState から開始する)
// TODO: ループを使用して Change() メソッドを n 回呼び出す
}
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。