本稿はコード設計に興味があるエンジニアを対象としています。
有名なデザインパターンで必ず取り上げられるもののひとつに、オブザーバーパターンがあります。
オブザーバーパターンは、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());
Unreal Engine なら、マルチキャストデリゲートを使えば、簡単に実装できます。
Unity の場合は C# 標準機能の delegate を使えば同じことができます。
