🥫Stateパターン (自販機の例) [C#]

State Pattern C#

Stateパターンとは?

Stateパターンは、オブジェクトの「状態」をクラスとしてカプセル化し、状態遷移(状態の変化)に応じてオブジェクトの振る舞いを変えるデザインパターンです。

なぜ使うのか?

通常、状態によって処理を変える場合は if 文や switch 文を多用しがちです。しかし、状態が増えるたびに条件分岐が複雑になり、コードの保守性が低下します。Stateパターンを使うことで、「状態ごとのロジック」を個別のクラスに閉じ込めることができ、コードがスッキリします。

1. 状態のインターフェース (IState)

すべての状態で共通のアクション(お金を入れる、ボタンを押すなど)を定義し、何を目的としているかを明確にします。

C#
/// <summary>
/// 自動販売機の状態を定義するインターフェース
/// </summary>
public interface IState
{
    // お金を入れる操作
    void InsertMoney();
    // ボタンを押す操作
    void PushButton();
}

2. 具体的な状態クラス(3つの状態)

各状態における振る舞いをクラスとして記述します。各クラスは「自分がその状態の時にどう振る舞うか」だけに集中しています。

C#
// --- お金が入っていない状態 ---
public class NoMoneyState : IState
{
    private readonly VendingMachine _machine;
    public NoMoneyState(VendingMachine machine) => _machine = machine;

    public void InsertMoney()
    {
        Console.WriteLine("→ お金が投入されました。");
        // 状態を「お金あり」へ移行
        _machine.SetState(_machine.HasMoneyState);
    }

    public void PushButton()
    {
        Console.WriteLine("× [警告] 先にお金を入れてください。");
    }
}

// --- お金が入っている状態 ---
public class HasMoneyState : IState
{
    private readonly VendingMachine _machine;
    public HasMoneyState(VendingMachine machine) => _machine = machine;

    public void InsertMoney()
    {
        Console.WriteLine("× [警告] 既にお金が入っています。");
    }

    public void PushButton()
    {
        Console.WriteLine("→ ボタンが押されました。商品を出力します。");
        
        // 在庫を1つ減らす
        _machine.ReduceStock();

        // 在庫状況に応じて次の状態を判定
        if (_machine.StockCount > 0)
        {
            _machine.SetState(_machine.NoMoneyState); // まだ在庫があれば待機へ
        }
        else
        {
            _machine.SetState(_machine.SoldOutState); // 在庫が切れたら売り切れへ
        }
    }
}

// --- 売り切れ状態 ---
public class SoldOutState : IState
{
    private readonly VendingMachine _machine;
    public SoldOutState(VendingMachine machine) => _machine = machine;

    public void InsertMoney()
    {
        Console.WriteLine("× [エラー] 売り切れです。お金は入りません。");
    }

    public void PushButton()
    {
        Console.WriteLine("× [エラー] 商品がないため、操作できません。");
    }
}

3. コンテキストクラス(自動販売機本体)

このクラスは「現在の状態」を保持し、処理を委譲(丸投げ)する役割です。

C#
/// <summary>
/// 自動販売機本体。各状態のインスタンスと在庫を管理する。
/// </summary>
public class VendingMachine
{
    // 各状態のインスタンス(外部から切り替えられるよう公開)
    public IState NoMoneyState { get; }
    public IState HasMoneyState { get; }
    public IState SoldOutState { get; }

    // 現在の状態
    private IState _currentState;
    
    // 商品在庫数
    public int StockCount { get; private set; }

    public VendingMachine(int initialStock)
    {
        // 1. 各状態の初期化
        NoMoneyState = new NoMoneyState(this);
        HasMoneyState = new HasMoneyState(this);
        SoldOutState = new SoldOutState(this);

        StockCount = initialStock;

        // 2. 初期状態の設定
        _currentState = (initialStock > 0) ? NoMoneyState : SoldOutState;
    }

    // 状態を切り替えるメソッド
    public void SetState(IState newState)
    {
        _currentState = newState;
    }

    // 在庫を減らす処理
    public void ReduceStock()
    {
        if (StockCount > 0) StockCount--;
        Console.WriteLine($"   (現在の在庫: {StockCount})");
    }

    // --- 外部API(利用者からの操作) ---
    // 実際の処理は「現在の状態クラス」へ完全に委譲(Delegation)する
    public void InsertMoney() => _currentState.InsertMoney();
    public void PushButton() => _currentState.PushButton();
}

実行イメージ(Mainメソッド)

C#
class Program
{
    static void Main()
    {
        // 自動販売機を作成(商品在庫数 2で初期化)
        var machine = new VendingMachine(2);

        // 1回目:購入
        machine.InsertMoney();
        machine.PushButton();

        // 2回目:購入(これで在庫が切れる)
        machine.InsertMoney();
        machine.PushButton();

        // 3回目:売り切れ後に操作してみる
        machine.InsertMoney();
        machine.PushButton();
    }
}

Stateパターンのまとめ

  • 条件分岐が消えた: VendingMachine クラスの中に if (stock == 0)if (isMoneyInserted) といったフラグ管理が一切ありません。
  • 状態の独立性: 「売り切れの時にどう動くか」を修正したい場合は、SoldOutState クラスだけを見れば完結します。
  • 拡張性: 例えば「メンテナンス中」という状態を増やしたい時は、新しいクラスを作って SetState するだけで済み、既存の「お金あり」などのロジックを壊す心配がありません。