1. はじめに
例えば、ボタンや我々のWorkerのように何らかの動作をするオブジェクトがあるとします。一方で、その動作に反応すべき他のオブジェクトが多数存在します。もしWorkerクラス内にすべての可能な「リスナー」をハードコードしてしまうと、そのコードの保守は地獄になります:サブスクライバーの一覧を変更するたびにWorker自体を修正する必要が出てきます。
これは開放閉鎖の原則(OCP)に違反し、設計上良くないプラクティスです。
オブザーバーパターン:基本的な考え方
パターン「オブザーバー」(Observer)はこの問題を解決します。Publisherオブジェクトは、誰がリスナーで何をするかを知らなくても、関心のある任意の数のリスナーに変更を通知できます。Publisherは単に「通知」を投げ、興味のある者が好きなように反応します。
アナロジー:ニュースレターの購読。編集部やチャンネル(Publisher)が新しいメールを送れば、すべての購読者(オブザーバー)がそれを受け取る。編集部は誰が購読しているかを知らなくていいし、知る必要もありません。
豆知識:「オブザーバー」は非常に一般的で、GoF(バンド・オブ・フォー)のデザインパターンにも含まれています。
C#におけるObserver:イベントとデリゲートでの具現化
C#では、オブザーバーパターンはイベントとデリゲートの仕組みで「標準装備」されています。イベントはさまざまなハンドラがサブスクライブできる「拡張ポイント」です。サブスクライバーのリストを手動で管理する代わりに、言語のイベント機構がそれを扱ってくれます。以下でまず「手動」実装を見てから、イベントを使った実装を見ていきます。
2. イベントを使わない古典的なObserver実装
もし言語にイベントがなかったら、どう書くか見てみましょう:
// インターフェイスのオブザーバー
public interface IObserver
{
void Update(string message);
}
// Publisher
public class Worker
{
private List<IObserver> observers = new List<IObserver>();
public void Subscribe(IObserver observer)
{
observers.Add(observer);
}
public void Unsubscribe(IObserver observer)
{
observers.Remove(observer);
}
public void DoWork()
{
Console.WriteLine("Worker は動作中...");
NotifyObservers("作業が完了しました!");
}
private void NotifyObservers(string message)
{
foreach (var observer in observers)
{
observer.Update(message);
}
}
}
// 具象オブザーバー
public class WorkListener : IObserver
{
public void Update(string message)
{
Console.WriteLine($"
WorkListener がメッセージを受け取りました: {message}");
}
}
初期化:
var worker = new Worker();
var listener = new WorkListener();
worker.Subscribe(listener);
worker.DoWork();
補足:ここではサブスクライバーのリスト(List<IObserver> observers)を手動で管理しており、Subscribe/Unsubscribeメソッドで明示的に登録解除を行います。
3. イベントとデリゲート — C#流の高レベルなObserver実装
同じことをイベントを使ってもっとシンプルかつエレガントに実現できます。これがC#スタイルのObserverです:
public class Worker
{
public event EventHandler<WorkCompletedEventArgs>? WorkCompleted;
public void DoWork()
{
Console.WriteLine("Worker は動作中...");
OnWorkCompleted("作業が完了しました!");
}
protected virtual void OnWorkCompleted(string message)
{
WorkCompleted?.Invoke(this, new WorkCompletedEventArgs { Message = message });
}
}
public class WorkCompletedEventArgs : EventArgs
{
public string Message { get; set; }
}
public class WorkListener
{
public void OnWorkCompleted(object? sender, WorkCompletedEventArgs e)
{
Console.WriteLine($"WorkListener がメッセージを受け取りました: {e.Message}");
}
}
// サブスクライブ:
var worker = new Worker();
var listener = new WorkListener();
worker.WorkCompleted += listener.OnWorkCompleted;
worker.DoWork();
このアプローチの利点:
- サブスクライバーのリストを手動で管理する必要がない。
- イベントの機能(複数サブスクライブ、解除、ラムダ)が使える。
- 安全性が確保される:イベントを呼び出せるのは基本的にPublisherだけ。
- 疎結合:Publisherはリスナーについて何も知らない。
4. オブザーバーをアプリに組み込む方法
パターンObserverをコンソールアプリに組み込んでみましょう。Workerはいくつでもハンドラを持てます。例えばコンソールに書くだけの者、作業数をカウントする者、メールを送る者("シェフ!全部終わったよ!")など、反応はそれぞれ異なります。
コードを例で拡張
// 2つ目のカウンターリスナー
public class WorkCounter
{
public int Count { get; private set; }
public void OnWorkCompleted(object? sender, WorkCompletedEventArgs e)
{
Count++;
Console.WriteLine($"作業が記録されました。合計: {Count} 回実行済み。");
}
}
// オブジェクト作成
var worker = new Worker();
var listener = new WorkListener();
var counter = new WorkCounter();
// 両方をサブスクライブ
worker.WorkCompleted += listener.OnWorkCompleted;
worker.WorkCompleted += counter.OnWorkCompleted;
// 複数の作業をシミュレート
worker.DoWork();
worker.DoWork();
// Output:
// Worker は動作中...
// WorkListener がメッセージを受け取りました: 作業が完了しました!
// 作業が記録されました。合計: 1 回実行済み。
// Worker は動作中...
// WorkListener がメッセージを受け取りました: 作業が完了しました!
// 作業が記録されました。合計: 2 回実行済み。
このように、必要に応じてオブザーバーを追加しても、Workerのコードは一切変更しません。Workerクラスは不変で、システム全体の振る舞いはサブスクライバーによって拡張されます。
5. 便利な注意点
実例:GUIにおけるObserver
オブザーバーパターンはあらゆるGUIフレームワークの基礎です。Windows FormsやWPFではボタンのクリックがClickイベントを発火します。あなたはそのイベントに反応するハンドラ(オブザーバー)を書くだけでよく、Buttonクラスや.NETライブラリがあなたのサブスクライバーについて知る必要はありませんし、知りません。
// WPFやWinFormsの例(だいたいこんな感じ)
myButton.Click += (s, e) => MessageBox.Show("ユーザーがボタンをクリックしました!");
実プロジェクトでのObserverの用途
- ユーザーインターフェイス(クリックや変更、タイマーなどへの反応)。
- 通知システムやイベントシステム。
- 拡張可能なシステムのプラグイン(コアがイベントを発行し、拡張がサブスクライブする)。
- 分散システムやゲームエンジン(疎結合なリアクティブチェーン)。
要するに、ある部分が他の部分の変化に反応できる拡張性のあるシステムが必要なら、Observer must have!
7. 特徴、典型的なミスとその回避
Observer利用時の潜在的な問題
メモリリーク。サブスクライバーがイベントにサブスクライブしたまま解除しないと(特に長寿命のオブジェクトで)、ガベージコレクタはそのオブジェクトを解放できません。なぜならPublisherがイベントデリゲート経由で参照を保持しているからです。サブスクライバーが不要になったのにPublisherが存続している場合、これは重大です。
多重サブスクライブ。同じハンドラが二重にサブスクライブされると、2回呼ばれてしまい、動作が重複したり思わぬ副作用が発生します。
ハンドラ内の例外。もしあるハンドラが例外を投げると、次のサブスクライバーの実行が中断される可能性があります。ハンドラの堅牢性を考え、必要なら個々のハンドラ呼び出しをtry-catchで囲んで、たとえ一つが失敗しても他が実行されるように設計しましょう。
よくあるリークのパターン
flowchart LR
Publisher["Publisher(Worker)"] -- イベント --> ObserverA["リスナー A(生存中)"]
Publisher -- イベント --> ObserverB["リスナー B(リーク:Unsubscribeを忘れた)"]
GO TO FULL VERSION