メインコンテンツまでスキップ

第11章 State(状態)と合成:if地獄を減らす🚦🙂

この章は「状態が増えるほど、if/switch が増えてツラくなる問題」を、合成(Composition)でスッキリ解決する入口だよ〜🧩💡 State パターンは「オブジェクトの状態によって振る舞いが変わる」のを、クラス分割で扱いやすくする考え方だよ(まるで“クラスが変わった”みたいに見える、って説明されることが多い✨) (リファクタリング・グル )


今日のゴール🎯✨

  • ✅ if/switch が増える理由を、体験でわかる
  • ✅ 「状態」「イベント」「遷移(せんい)」を、表で整理できる🗺️
  • ✅ State(状態オブジェクト)に分けて、合成で差し替える形にできる🧩
  • ✅ **禁止遷移(ありえない操作)**をコードで守れる🛑
  • ✅ テストがラクになる感覚もつかむ✅⚡

題材:注文(Order)の状態📦🛒

よくあるやつ!注文って状態が増えがちだよね〜🙂

  • 下書き(Draft)📝
  • 確定(Confirmed)✅
  • 発送(Shipped)🚚
  • 配達完了(Delivered)📬
  • キャンセル(Cancelled)❌

まずは “if/switch 地獄” の例😇🔥(わざとツラくする)

「enum + switch」で始めると、最初はラク。…でも状態が増えると急にキツい😵‍💫

public enum OrderStatus
{
Draft,
Confirmed,
Shipped,
Delivered,
Cancelled
}

public sealed class Order
{
public OrderStatus Status { get; private set; } = OrderStatus.Draft;

public void Confirm()
{
switch (Status)
{
case OrderStatus.Draft:
Status = OrderStatus.Confirmed;
return;
default:
throw new InvalidOperationException($"Confirmできない状態: {Status}");
}
}

public void Ship()
{
switch (Status)
{
case OrderStatus.Confirmed:
Status = OrderStatus.Shipped;
return;
default:
throw new InvalidOperationException($"Shipできない状態: {Status}");
}
}

public void Deliver()
{
switch (Status)
{
case OrderStatus.Shipped:
Status = OrderStatus.Delivered;
return;
default:
throw new InvalidOperationException($"Deliverできない状態: {Status}");
}
}

public void Cancel()
{
switch (Status)
{
case OrderStatus.Draft:
case OrderStatus.Confirmed:
Status = OrderStatus.Cancelled;
return;
default:
throw new InvalidOperationException($"Cancelできない状態: {Status}");
}
}
}

ここがツラくなるポイント😵‍💫💥

  • 😭 操作ごとに switch が散らばる(Confirm/Ship/Cancel…全部)
  • 😭 新しい状態を追加すると、あちこちの switch を修正
  • 😭 ルール(禁止遷移)が増えるほど、例外・分岐が増殖🌱➡️🌳
  • 😭 状態別の処理(通知、課金、在庫…)が混ざってくる🧨

状態遷移を “表” で整理🗺️✨(ここ超大事!)

State Transition

まず設計をラクにするコツはこれ👇 **「状態(State)」「イベント(操作)」「次の状態」**を表にしちゃう😊

今の状態Confirm ✅Ship 🚚Deliver 📬Cancel ❌
Draft 📝ConfirmedCancelled
Confirmed ✅ShippedCancelled
Shipped 🚚Delivered
Delivered 📬
Cancelled ❌

✖ は「禁止遷移」🛑(ここをコードで守りたい!)

🤖AI活用:この表、AIに作らせると速いよ! 例:「注文の状態を Draft/Confirmed/Shipped/Delivered/Cancelled として、Confirm/Ship/Deliver/Cancel の遷移表を作って。禁止遷移も明記して」


解決方針:状態を “クラス” にする🚦➡️🧩

State Pattern

State パターンの超ざっくりイメージ👇

  • Order は「いまの状態オブジェクト」を 持つ(合成) 🧩
  • Order の操作は「状態オブジェクトに委譲」💁‍♀️
  • 状態ごとに「できる/できない」「次の状態」を閉じ込める📦

実装してみよう🛠️✨(Stateオブジェクト版)

1) 状態インターフェースを作る🔌🙂

public interface IOrderState
{
string Name { get; }
void Confirm(Order order);
void Ship(Order order);
void Deliver(Order order);
void Cancel(Order order);
}

2) Order(本体)は “委譲するだけ” に寄せる🧩✨

public sealed class Order
{
private IOrderState _state;

public Order()
{
_state = new DraftState();
}

public string StatusName => _state.Name;

// 状態変更はここだけに集約🎯
internal void ChangeState(IOrderState newState)
=> _state = newState;

public void Confirm() => _state.Confirm(this);
public void Ship() => _state.Ship(this);
public void Deliver() => _state.Deliver(this);
public void Cancel() => _state.Cancel(this);
}

ポイント🎯

  • Order は「状態を持つ」だけ(合成)🧩
  • 「どう振る舞うか」は状態クラス側へ🚦✨
  • ChangeStateinternal にして、勝手に外から変えにくくする🛡️

3) 状態クラスを作る🚦✨(禁止遷移もここ!)

Draft 📝

public sealed class DraftState : IOrderState
{
public string Name => "Draft";

public void Confirm(Order order) => order.ChangeState(new ConfirmedState());

public void Cancel(Order order) => order.ChangeState(new CancelledState());

public void Ship(Order order) => throw new InvalidOperationException("Draftでは発送できません🛑");
public void Deliver(Order order) => throw new InvalidOperationException("Draftでは配達完了にできません🛑");
}

Confirmed ✅

public sealed class ConfirmedState : IOrderState
{
public string Name => "Confirmed";

public void Ship(Order order) => order.ChangeState(new ShippedState());

public void Cancel(Order order) => order.ChangeState(new CancelledState());

public void Confirm(Order order) => throw new InvalidOperationException("すでに確定済みです🛑");
public void Deliver(Order order) => throw new InvalidOperationException("Confirmedでは配達完了にできません🛑");
}

Shipped 🚚

public sealed class ShippedState : IOrderState
{
public string Name => "Shipped";

public void Deliver(Order order) => order.ChangeState(new DeliveredState());

public void Confirm(Order order) => throw new InvalidOperationException("発送後に確定はできません🛑");
public void Ship(Order order) => throw new InvalidOperationException("すでに発送済みです🛑");
public void Cancel(Order order) => throw new InvalidOperationException("発送後のキャンセルは不可です🛑");
}

Delivered / Cancelled(終端)🏁

public sealed class DeliveredState : IOrderState
{
public string Name => "Delivered";

public void Confirm(Order order) => throw new InvalidOperationException("配達完了後は操作できません🛑");
public void Ship(Order order) => throw new InvalidOperationException("配達完了後は操作できません🛑");
public void Deliver(Order order) => throw new InvalidOperationException("配達完了後は操作できません🛑");
public void Cancel(Order order) => throw new InvalidOperationException("配達完了後は操作できません🛑");
}

public sealed class CancelledState : IOrderState
{
public string Name => "Cancelled";

public void Confirm(Order order) => throw new InvalidOperationException("キャンセル後は操作できません🛑");
public void Ship(Order order) => throw new InvalidOperationException("キャンセル後は操作できません🛑");
public void Deliver(Order order) => throw new InvalidOperationException("キャンセル後は操作できません🛑");
public void Cancel(Order order) => throw new InvalidOperationException("すでにキャンセル済みです🛑");
}

使ってみる🧪✨

var order = new Order();
Console.WriteLine(order.StatusName); // Draft

order.Confirm();
Console.WriteLine(order.StatusName); // Confirmed

order.Ship();
Console.WriteLine(order.StatusName); // Shipped

order.Deliver();
Console.WriteLine(order.StatusName); // Delivered

これで何が嬉しいの?🥳🎁

  • ✅ 状態ごとのルールが そのクラスに閉じる(散らばらない)📦
  • ✅ ある状態を直しても、他の状態への影響が小さい🛡️
  • ✅ 「新しい状態の追加」がやりやすい(クラス追加が中心)🧩
  • ✅ Order 本体がスッキリ(合成して委譲するだけ)🧼✨

よくある落とし穴⚠️😵‍💫(回避しよ!)

落とし穴1:状態クラスが“便利屋”になる🧰💥

→ 状態に入れるのは「状態によって変わる振る舞い」だけ! (DB保存とか、通知の送信とか、別の部品に任せるのが基本だよ🧩)

落とし穴2:遷移がどこで起きるか分からなくなる🌀

ChangeState は Order に集約🎯 → 状態クラスは「いつ・どれに変えるか」だけ責任を持つ🚦


ちょい実践:状態で “副作用” が起きる例📣💳(超ミニ)

「Confirmed になったら通知したい」みたいなやつね🙂 これは 状態が依存する部品(通知)を注入して使う形がキレイ🧩🎁 (第8〜10章のDI/Strategyの流れがここで効くよ〜!)

イメージだけ置いとくね👇(説明は軽め)

public interface INotifier { void Notify(string message); }

public sealed class ConfirmedState : IOrderState
{
private readonly INotifier _notifier;
public ConfirmedState(INotifier notifier) => _notifier = notifier;

public string Name => "Confirmed";

public void Ship(Order order) => order.ChangeState(new ShippedState());

public void Cancel(Order order) => order.ChangeState(new CancelledState());

public void Confirm(Order order) => throw new InvalidOperationException("すでに確定済みです🛑");
public void Deliver(Order order) => throw new InvalidOperationException("Confirmedでは配達完了にできません🛑");
}

テストがラクになるご褒美🍬✅(ミニ体験)

Stateパターンだと「この状態でこの操作したらこうなる」が 素直にテストできるよ🙂✨

using Xunit;

public class OrderStateTests
{
[Fact]
public void Draft_can_confirm()
{
var order = new Order();
order.Confirm();
Assert.Equal("Confirmed", order.StatusName);
}

[Fact]
public void Shipped_cannot_cancel()
{
var order = new Order();
order.Confirm();
order.Ship();

Assert.Throws<InvalidOperationException>(() => order.Cancel());
}
}

🤖AI活用: 「この状態遷移のテストケースを10個出して。境界(禁止遷移)多めで!」って頼むと捗るよ🧪✨


🤖AI活用プロンプト集(コピペOK)🧠💬

  • 「注文の状態遷移表を作って。禁止遷移も入れて、表形式で」
  • 「Stateパターンで、Order/Stateインターフェース/各Stateクラスの雛形を出して」
  • 「この設計で“責務が混ざってる臭い”がする場所を指摘して」
  • 「テストケース案を列挙して。正常系と禁止遷移を半々で」
  • 「例外メッセージをユーザー向けに分かりやすくして(短め&日本語)」

いつ State を使う?判断のコツ🎯🙂

State が効くサイン✅

  • 状態が増えた/増えそう🌱
  • if/switch が増殖してきた🔥
  • 「この状態ではこの操作禁止」が多い🛑
  • 状態によって同じ操作の意味が変わる🔁

逆に、やりすぎ注意のサイン⚠️

  • 状態が2〜3個で固定っぽい🙂
  • ルールがほぼ増えない → その場合は enum + switch のままでも全然OKだよ🙆‍♀️

おまけ:C# 14 で “読みやすさ” をちょい足し🍬✨

C# 14 は .NET 10 と一緒に来ていて、**extension members(拡張メンバー)**みたいな新機能も入ってるよ(拡張プロパティも書ける!) (Microsoft Learn) これを使うと、状態チェックを自然な名前で書けたりする🙂

例(超ミニ)👇

public static class OrderExtensions
{
extension(Order order)
{
public bool IsFinished => order.StatusName is "Delivered" or "Cancelled";
}
}

// 使い方:
var order = new Order();
if (!order.IsFinished) { /* まだ処理できる */ }

まとめ🌈✨

  • 「状態が増える」= if/switch が増えやすい😇🔥
  • State は 状態ごとのルールをクラスに閉じ込める🚦📦
  • Order は 状態オブジェクトを合成して委譲する🧩💁‍♀️
  • 禁止遷移をコードで守れるから、事故が減る🛑✨
  • テストも気持ちよくなる🍬✅

ミニ宿題📮🙂

「返品(Returned)↩️」を追加してみて!

  • Delivered → Returned はOK
  • Returned → Refund(返金)💴 を追加してもOK
  • それ以外は基本禁止🛑 最後に「遷移表」を更新して、Stateクラスに反映してね🗺️✨