第66章:Observer ①:変化を知らせる📣
ねらい 🎯

Observer は、ある場所で起きた変化(イベント)を、関係者(購読者)に“疎結合”で通知するための考え方だよ〜!🔔✨
C# では event / EventHandler がまさに定番の実装手段で、発行者(publisher) と 購読者(subscriber) をゆる〜くつなげられるのが強み😊
(“通知したい相手が増えても、発行者側のコードをいじらず足せる” がキモ!) (Microsoft Learn)
到達目標 🌸
この章が終わったら、次ができるようになるよ〜!✨
- Observer が解く困りごとを、**「何が変化して」「誰に知らせたいか」**で説明できる🙂
event EventHandler<TEventArgs>で、C#の“定番の形”でイベントを作れる🔔 (Microsoft Learn)- 購読(
+=)と購読解除(-=)の大切さ(メモリリーク回避)を説明できる🧯😵 - 「イベントの契約」= いつ通知?何を渡す?例外は?重い処理は? を決められる📝✨
手順 🧭✨
1) “変化”と“知らせたい人”を分けて書く 📝
まずは超シンプルにこれだけ👇
- 変化(イベント)例:注文が 確定した / 支払い済みになった / 発送された 🛒📦
- 知らせたい人(購読者)例:メール通知、監査ログ、画面更新、ポイント付与…📧🧾🖥️🎁
ここで大事なのは、発行者が購読者を直接呼ばないこと!
(発行者が EmailSender とか知り始めると、依存が太って地獄🔥)
2) C#の“標準イベントパターン”で設計する 🔔✨
C#/.NET には「こう書くのが定番だよ」ってガイドがあるよ〜📚
- イベント型は
EventHandler<TEventArgs>を使う(独自デリゲートを増やしにくい) (Microsoft Learn) - 渡す情報は
EventArgsの派生クラスにする(あとで情報が増えても困らない) (Microsoft Learn)
3) 最小の“発行者(publisher)”を書いてみる 🧩
例:Order(注文)が状態変化を通知するよ〜📣
public enum OrderStatus
{
New,
Paid,
Shipped
}
public sealed class OrderStatusChangedEventArgs : EventArgs
{
public OrderStatusChangedEventArgs(OrderStatus before, OrderStatus after)
{
Before = before;
After = after;
}
public OrderStatus Before { get; }
public OrderStatus After { get; }
}
public sealed class Order
{
public OrderStatus Status { get; private set; } = OrderStatus.New;
// ✅ 定番:EventHandler<TEventArgs>
public event EventHandler<OrderStatusChangedEventArgs>? StatusChanged;
public void MarkPaid()
{
if (Status != OrderStatus.New)
throw new InvalidOperationException("New の注文だけ支払い可能です");
var before = Status;
Status = OrderStatus.Paid;
OnStatusChanged(before, Status);
}
private void OnStatusChanged(OrderStatus before, OrderStatus after)
{
// ✅ event の起動(購読者がいれば呼ばれる)
StatusChanged?.Invoke(this, new OrderStatusChangedEventArgs(before, after));
}
}
ポイント💡
eventは「外からInvokeできない」「勝手に代入できない」など、安全な公開になりやすいよ〜🛡️publisher/subscriberの言い方は .NET ドキュメントでも定番! (Microsoft Learn)
4) “購読者(subscriber)”を書いてみる 👂✨
例:支払いになったら「ログに書く」購読者🧾
public sealed class OrderAuditLogger : IDisposable
{
private readonly Order _order;
public OrderAuditLogger(Order order)
{
_order = order;
_order.StatusChanged += OnStatusChanged; // ✅ 購読
}
private void OnStatusChanged(object? sender, OrderStatusChangedEventArgs e)
{
if (e.After == OrderStatus.Paid)
{
Console.WriteLine("AUDIT: 注文が支払い済みになりました 🧾✅");
}
}
public void Dispose()
{
// ✅ 超重要:購読解除(後で理由を説明するよ)
_order.StatusChanged -= OnStatusChanged;
}
}
5) “イベントの契約”を決める(めっちゃ大事)📜✨
イベントは「呼べば勝手に良い感じになる魔法」じゃなくて、契約だよ〜🙂↕️
最低限これを決めよう👇
- いつ発火?(状態変更の直後?前?)⏱️
- 何を渡す?(IDだけ?Before/After?)🎁
- 例外はどうする?(購読者が投げたら止める?他も動かす?)💥
- 重い処理は?(イベント内で長時間処理しない)🐢
ちなみにイベントは multicast(複数呼び出し)で、購読順に呼ばれるのが基本だよ〜(でも順序に依存した設計は危険⚠️) (Microsoft Learn)
6) AI補助(Copilot/Codex)で雛形を作るなら 🤖✍️
雛形作りは速いけど、レビューは人間がやるやつ!👀✨ おすすめプロンプト例👇
- 「
OrderにStatusChangedevent を追加。EventHandler<TEventArgs>で。EventArgs派生クラスに Before/After。購読解除をIDisposableに入れて。外部フレームワーク作らないで」
チェック観点🧠
eventが public で、Invokeは class 内だけ?TEventArgsが必要最小?- 購読解除できる導線ある?(Dispose など)
よくある落とし穴 ⚠️😵
落とし穴1:購読解除しなくてメモリリーク🧟♀️
長生きする publisher(例:アプリ全体で生きるサービス)が、短命な subscriber(例:画面)を購読したままだと、 subscriber が GC されにくくなって残り続けることがあるよ〜😱
WPF みたいなUI世界では、これを避けるために 弱いイベント(Weak Event) の仕組みも使われるよ〜🪶 (Microsoft Learn)
✅ 対策の基本
Dispose()などで-=する- “誰がいつ解除するか” を決める(契約)📝
落とし穴2:イベントの中で重い処理をする🐢💤
イベントは基本「その場で呼ばれる」ので、購読者が重い処理をすると publisher 側が詰まるよ〜💦 (ログや送信はキューに積む、などにしたくなるけど、その話は後の章で育てよう🌱)
落とし穴3:通知データに“内部構造”を渡しすぎる🧨
たとえば List<OrderLine> まるごと渡すとか、可変の参照を渡すと事故る…😵
まずは ID / Before-After / 必要最小でOK!✨
(足りなくなったら EventArgs を拡張できるのが、標準パターンの良さだよ〜) (Microsoft Learn)
落とし穴4:購読者の例外で全部止まる💥
イベント呼び出し中に購読者が例外を投げると、publisher まで波及して止まることがあるよ〜😱 対策は「契約として許す/許さない」を決めること!
- 例外は上に伝播して止める(バグを早期発見)🧯
- 例外は握りつぶして継続(“他の購読者は動かしたい”用途)🔁
後者をやるなら、GetInvocationList() で1つずつ呼ぶ方法もあるよ〜(順序や多重呼び出しの理解にも役立つ✨) (Microsoft Learn)
演習(10〜30分)🧪🍰
“注文の変化”を Observer で通知してみよ〜!📣✨
お題 🎯
-
OrderにStatusChangedイベントを作る(この章のコードでOK)🔔 -
購読者を2つ作る
OrderAuditLogger(ログ出す)🧾OrderBadgeUpdater(「支払い済み✅」って表示する想定でConsole.WriteLineでもOK)🏷️
テスト(MSTest例)🧪
イベントがちゃんと飛んだかをテストで固定しよ〜!✨
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class ObserverTests
{
[TestMethod]
public void MarkPaid_raises_StatusChanged_once()
{
var order = new Order();
int callCount = 0;
OrderStatus? before = null;
OrderStatus? after = null;
order.StatusChanged += (_, e) =>
{
callCount++;
before = e.Before;
after = e.After;
};
order.MarkPaid();
Assert.AreEqual(1, callCount);
Assert.AreEqual(OrderStatus.New, before);
Assert.AreEqual(OrderStatus.Paid, after);
}
}
追加チャレンジ 🔥
Dispose()で購読解除したら、もう通知されないことをテストしてみてね🧯✨
自己チェック ✅🌟
- Observer を 「変化の発行者」+「購読者」 で説明できる?📣👂 (Microsoft Learn)
event EventHandler<TEventArgs>を使って、定番の形で書けた?🔔 (Microsoft Learn)- 購読解除(
-=)が必要な理由を言える?🧟♀️→🧯 (Microsoft Learn) - 「イベントの契約(いつ/何/例外/重さ)」を決めた?📜✨
- イベントの順序に依存してない?(依存してたら危険⚠️) (Microsoft Learn)