Observer パターンは YouTube に例えれば分かる

本稿はコード設計に興味があるエンジニアを対象としています。

オブザーバー(Observer)パターンとは?

有名なデザインパターンで必ず取り上げられるもののひとつに、オブザーバーパターンがあります。
オブザーバーパターンは、YouTube の配信者と視聴者(リスナー)の関係に似ています。

オブザーバーパターンでは、サブジェクトとオブザーバーと呼ばれる、ふたつのクラスを扱います。
サブジェクトのひとつのインスタンスに対し、オブザーバーは複数のインスタンスがあることを前提としています。
サブジェクトの状態が更新されたとき、サブジェクトに紐づけた全てのオブザーバーが通知を受け取ります。

この場合、サブジェクトが配信者、視聴者がオブザーバーにあたります。

オブザーバーパターンの発展形に、パブリッシュサブスクライブ(Publish-Subscribe または Pub/Sub)パターンというものがありますが、本稿では扱いません。
どんな状況で使うのか?

「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]
				);
		}
	}
}

オブザーバーパターンの型

オブザーバーパターンにも、カラテの型みたいな、決まった形があります。

各メソッド名、戻り値、引数は任意です。
絶対にこうする必要がある…というものではありませんが、前述のサンプルコードを型にあてはめた形に修正してみます。

ConcreteSubject が GetState() と SetState() メソッドを持っていますが、これに該当する機能を持っているなら、これらのメソッドは必要ではありません。
型にあてはめたサンプルコード
/* 追加 */
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());

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です