本稿はコード設計に興味があるエンジニアを対象としています。
有名なデザインパターンで必ず取り上げられるもののひとつに、オブザーバーパターンがあります。
オブザーバーパターンは、YouTube の配信者と視聴者(リスナー)の関係に似ています。
オブザーバーパターンでは、サブジェクトとオブザーバーと呼ばれる、ふたつのクラスを扱います。
サブジェクトのひとつのインスタンスに対し、オブザーバーは複数のインスタンスがあることを前提としています。
サブジェクトの状態が更新されたとき、サブジェクトに紐づけた全てのオブザーバーが通知を受け取ります。
この場合、サブジェクトが配信者、視聴者がオブザーバーにあたります。
「YouTube のチャンネル通知みたいな仕組みが欲しい」と思ったときです。
もっと大雑把に言うと、あるデータやパラメータに変化があったときに、不特定多数のクラスで通知を受け取りたいときに使います。
どんなコードになるのかは、そのコードを書く環境で変わります。
同じプログラム言語を使っても、エンジンやシステムが違うならコードは変わります。
エンジンやシステムが同じでも、そのシステムの中で自分が担当している分野によってコードが変わります。
デザインパターンが具体的なコードにしにくいのは、デザインパターン自体が抽象的な概念だからです。
抽象的なものを具体的なコードに落とし込むにあたり、自分が置かれている環境に合わせる必要が出て来るため、サンプルコードを書いて「これがオブザーバーパターンだ!」と言うのは誤解を生みます。
そうは言っても、コードを見てみないと初心者には想像しにくいので、以下の環境に限定したサンプルコードを示します。
- C# のコンソールアプリとして作成するためのコード。
- .Net 8.0 を使用し、それ以外の拡張機能や API は使わない。
- Windows 11 Home で動作する。
- オブザーバーパターンのイメージをつかむことを目的としたプライベートかつ学習目的のコードで、様々な環境で幅広く利用されることは想定していない。
- コードがオブザーバーパターンの概念を正しく表現している保証はない。
- 業務用のコードに組み込めるようなレベルのものではない。
2024/08/01 動作が怪しいところ、分かりにくかったところを修正。
// サブジェクト class TuberChannel { // チャンネル登録する public void Subscribe(Listener subscriber) { if (!listeners.Contains(subscriber)) { listeners.Add(subscriber); } } // チャンネル登録者数を返す public int NumberOfSubscribers => listeners.Count; // Tuber の作業が進んだときに呼ばれる public void OnStateChanged(Tuber.State state) { // 新着動画を投稿していないなら登録者への通知はない if (state != Tuber.State.PostedMovie) { return; } // 新着動画が投稿されたので全ての登録者に通知を送る foreach (Listener listener in listeners) { listener.OnChannelUpdated(); } } // 全ての登録者への通知状況を返す public List<bool> ListenersNotifyStates { get { List<bool> result = new(); foreach (Listener listener in listeners) { result.Add(listener.HasNotified); } return result; } } List<Listener> listeners = new(); } // オブザーバー class Listener { // チャンネルが更新されたときに呼ばれる public void OnChannelUpdated() { hasNotified = true; } public bool HasNotified { get { if (hasNotified) { hasNotified = false; return true; } return false; } } bool hasNotified = false; } // Tuber がデータ(動画)の更新作業を行う class Tuber { static readonly int STATE_POSTED_MOVIE_VALUE = 2; static readonly int STATE_FINISHED_NOTIFY_VALUE = STATE_POSTED_MOVIE_VALUE + 1; // Tuber の新しいチャンネルを作り、内部で保持して返す public TuberChannel CreateChannel() { var new_channel = new TuberChannel(); channel = new_channel; return (TuberChannel)new_channel; } // Tuber の作業を進める public void Work() { if (channel == null) { return; } // 先に CreateChannel() する channel.OnStateChanged(this.CurrentState); if (state++ > STATE_FINISHED_NOTIFY_VALUE) { // 通知が終わったら進行状態をリセット state = 0; } } public enum State { MakingMovie, PostedMovie, FinishedNotify, } public State CurrentState { get { if (state == STATE_POSTED_MOVIE_VALUE ) { return State.PostedMovie; } if (state == STATE_FINISHED_NOTIFY_VALUE) { return State.FinishedNotify; } return State.MakingMovie; } } TuberChannel? channel = null; int state = 0; } class Program { static void Main() { // Tuber とチャンネルを作成 Tuber tuber = new (); TuberChannel channel = tuber.CreateChannel(); // 視聴者をチャンネルに追加 const int number_of_subscribers = 5; AddSubscribersToChannel(channel, number_of_subscribers); do { // Tuber の状況を表示 ShowTuberState(tuber, channel); // Tuber が働く tuber.Work(); // リスナーの状況(通知が来たか?)を表示 ShowListenerState(channel); } while (ShouldContinue(Console.ReadLine())); } static bool ShouldContinue(string input) { input = input.Trim(); if (input.Length > 0) { input = input.ToLower(); } return input != "q" && input != "e" && input != "x"; } static void AddSubscribersToChannel(TuberChannel channel, int number_of_subscribers) { for (int i = 0; i < number_of_subscribers; i++) { channel.Subscribe(new Listener()); } } static void ShowTuberState(Tuber tuber, TuberChannel channel) { Console.WriteLine("Tuberの状況:"); Console.WriteLine("\tチャンネル登録者数 {0}", channel.NumberOfSubscribers); switch (tuber.CurrentState) { case Tuber.State.MakingMovie: Console.WriteLine("\t動画を作成しています。"); break; case Tuber.State.PostedMovie: Console.WriteLine("\t動画を投稿しました。"); break; default: Console.WriteLine("\t休憩中です。"); break; } } static void ShowListenerState(TuberChannel channel) { Console.WriteLine("リスナーの状況"); string[] messages = { "通知なし。", "通知を受け取りました。" }; List<bool> notify_states = channel.ListenersNotifyStates; for (int i = 0; i < notify_states.Count; i++) { Console.WriteLine("\tリスナー{0}: {1}", i + 1, messages[notify_states[i] ? 1 : 0] ); } } }
オブザーバーパターンにも、カラテの型みたいな、決まった形があります。
各メソッド名、戻り値、引数は任意です。
絶対にこうする必要がある…というものではありませんが、前述のサンプルコードを型にあてはめた形に修正してみます。
/* 追加 */ interface Observer { void Update(); } /* 追加 */ interface Subject { void RegisterObserver(Observer _); void RemoveObserver(Observer _); void NotifyObservers(); } class TuberChannel : Subject { /* 修正 Subject を継承 */ // チャンネル登録する /* 修正 Subscribe を RegisterObserver に変更、処理も修正 */ public void RegisterObserver(Observer _) { var subscriber = _ as Listener; if (!listeners.Contains(subscriber)) { listeners.Add(subscriber); } } // チャンネル登録を解除する /* 追加 */ public void RemoveObserver(Observer _) { /* 略 */ } // 全てのオブザーバーに通知を送る /* 追加 */ public void NotifyObservers() { if (currentState != Tuber.State.PostedMovie) { return; } foreach (Listener listener in listeners) { listener.Update(); } } // チャンネル登録者数を返す /* 変更なし */ // 作業が進んだときに呼ばれる public int OnStateChanged(Tuber.State state) { currentState = state; /* 修正 */ return 0; } // 全ての登録者への通知状況を返す public List<bool> ListenersNotifyStates { /* 変更なし */ } List<Listener> listeners = new(); /* 変更なし */ Tuber.State currentState = Tuber.State.None; /* 追加 */ } class Listener : Observer { /* 修正 Observer を継承 */ // チャンネルが更新されたときに呼ばれる /* 修正 OnChannelUpdated を Update に変更 */ public void Update() { /* 変更なし */ } /* 変更なし */ } // データ(動画)の更新作業を行う class Tuber { /* 変更なし */ public enum State { None, /* 追加 */ MakingMovie, /* 変更なし */ PostedMovie, /* 変更なし */ FinishedNotify, /* 変更なし */ } /* 変更なし */ } class Program { static void Main() { /* 変更なし */ do { // Tuber の状況を表示 ShowTuberState(tuber, channel); /* 変更なし */ // Tuber が働く tuber.Work(); /* 変更なし */ // チャンネル更新があればリスナーに通知を送る channel.NotifyObservers(); /* 追加 */ // リスナーの状況(通知が来たか?)を表示 ShowListenerState(channel); /* 変更なし */ } while (/* 変更なし */); } static bool ShouldContinue(string input) { /* 変更なし */ } static void AddSubscribersToChannel(TuberChannel channel, int number_of_subscribers) { for (int i = 0; i < number_of_subscribers; i++) { channel.RegisterObserver(new Listener()); /* 修正 */ } } static void ShowTuberState(TuberWork tuber, TuberChannel channel) { /* 変更なし */ } static void ShowListenerState(TuberChannel channel) { /* 変更なし */ } }
どこが変更されたり追加された部分なのか?を、前のサンプルと比較するのが面倒なので、コードに記載しています。
大きな違いは、TuberChannel と Listener がインターフェースを継承するようになった点くらいです。
あとは、細かい変更です。
インターフェースを使っているのは、クラスの結合を弱めるためで、例えば、Listener の上位互換 PremiumListener を追加しても同じロジックで処理できます。
class PremiumListener : Listener {} channel.RegisterObserver(new PremiumListener());