#Observerパターンとは
Observerパターンとは、「クラスから通知を発行する仕組み」と、「他のクラスから発行された通知を受け取る仕組み」を実現するためのデザインパターンです。
このデザインパターンは、主にクラス間のデータのやり取りをするときに使われますが、以下のような状況において特に有用です。
- 双方向ではなく、一方からもう一方へと__一方通行でデータを発行する場合__。
- 1回ではなく、複数回データを発行する場合。
- 任意のタイミングでデータを発行したい場合。
- 発行されたデータを、複数のクラスが同時に受け取りたい場合。
特に1番はObserverパターンの必須要件で、双方向のデータのやり取りには使用できません。
#ObserverとObservable
このデザインパターンでは、「データを受け取るクラス」と「データを発行するクラス」を明確に分けて考えます。
このうち、「データを受け取るクラス」のことを「Observer」、「データを発行するクラス」のことを「Observable」と呼びます。
__Observer__とは、「監視者」や「観察者」といった意味の英単語で、データを発行するクラス(Observable)からの通知を受け取る(つまり、通知を観察する、監視する)ことからその名が付けられています。
対して__Observable__とは、Observerの-able系なので、「観察可能」といった意味になります。
Observableは「観察者」であるObserverにデータを発行するので、Observerから「観察されることができる」、「観察可能」といったことから、この名がつけられています。
ObserverとObservableは非常に重要なので、以下のようにまとめます。
Observer | Observable | |
---|---|---|
単語の意味 | 観察者 | 観察可能 観察されることができる |
クラスの意味 | 通知を受け取るクラス | 通知を発行するクラス |
#基本的な仕組み
Observerパターンの基本的な仕組みは、次の3ステップとなります。
- ObserverがObservableにデータの発行先として登録する(=購読する)
- Observableは登録されたすべての発行先に値を発行する
- Observerは受け取った値を使用して任意の処理を実行する
次の項から詳しく説明します。
##1. ObserverがObservableを購読する
Observer(値を受信したいクラス)は、欲しい値を発行しているクラス(Observable)に対して、「僕に値を発行・通知してください」と__自分自身を通知先として登録します__。
この「配信登録」することを、「購読(Subscribe)する」と言います。
現実世界で「AさんはS社の雑誌を定期購読する」といった表現はよく使われます。
この場合も、AさんはS社に対して「僕に雑誌を定期的に送ってください」と自分自身を送付先として登録しますね。それと同じような意味になります。
そして、一つの値の発行元(Observable)に対して、複数のObserverが値の発行先として登録することができることも、Observerパターンの特徴の一つです。
##2.Observableは登録されたすべての発行先に値を発行する
Observable(値を発行するクラス)は、発行する値ができると、値の発行先として登録された__すべてのObserverに対して一斉に値を発行します__。
こうして、ObserverはObservableからの通知(値)を受け取ることができます。
##3.Observerは受け取った値を使用して任意の処理を実行する
Observableから発行された値を受け取ったObserverは、受け取った値を使用して任意の処理を実行することができます。
このようにして、ObservableからObserverへとデータの受け渡しが実現されます。
#Observerパターンを実装してみる
Observerパターンの基本的な仕組みの大枠は理解頂けたかと思います。
それでは、これらの仕組みを実際にどのように実装するかを説明していきます。
##IObserver<T>
インターフェイスとIObservable<T>
インターフェイス
Observerパターンは、「データを受け取るクラス」と「データを発行するクラス」を明確に分けて考えるデザインパターンと書きました。
実は、.NET Framework4.0以降には、Observerパターンで使えるIObserver<T>
インターフェイスとIObservable<T>
インターフェイスがSystem
名前空間に標準で用意されています。
Observerパターンを実装したい場合、これらのインターフェイスをデータの受信側、データの発行側のクラスにそれぞれ実装すれば良いことになります。
これらのインターフェイスの定義は次のようになっています。
public interface IObserver<in T>
{
//データの発行が完了したことを通知する
void OnCompleted();
//データの発行元でエラーが発生したことを通知する
void OnError(Exception error);
//データを通知する
void OnNext(T value);
}
public interface IObservable<out T>
{
//データの発行を購読する
IDisposable Subscribe(IObserver<T> observer);
}
IObserver<T>
インターフェイスのOnNext
メソッドは、__データを新しく通知する__際に呼び出すメソッドです。
OnCompleted
メソッドは、データの発行がすべて完了し、__これ以上通知するものがない場合__に呼び出します。
万が一、データの発行元で__何らかの例外が発生してしまった場合__は、OnError
メソッドを呼び出して、エラーが発生したことを通知します。
IObservable<T>
インターフェイスのSubscibe
メソッドは、__データの発行元からの通知を受け取りたいとき__に呼び出します。
引数には、通知の発行先となるIObserver<T>
オブジェクトを指定します。
ここで、以下のような違和感を覚える方がいらっしゃるかもしれません。
IObserver<T>
はデータを受け取るクラスが実装するのに、なぜデータを発行する系のメソッドを実装するのか?
逆にIObservable<T>
はデータを発行するクラスが実装するのに、なぜデータの発行を購読するメソッドを実装するのか?
その答えは、これらのメソッドは、__実装するクラス自身が使用するのではなく、お互いに相手が使用するものだから__です。
例えば、「データを受け取るクラス(Observer)」が実装したIObserver<T>
インターフェイスのOnNext
メソッドは、データを発行するクラス(Observable)が呼び出すことで、Observerに対して値を通知することができます。
逆に、「データを発行するクラス(Observable)」が実装したIObservable<T>
インターフェイスのSubscribe
メソッドは、データを受け取るクラス(Observer)が呼び出すことで、自分自身をデータの発行先として登録することができます。
まだ違和感が拭えないかもしれませんが、実際に実装してみると理解が深まるかもしれません。
それでは、実際にIObserver<T>
インターフェイスとIObservable<T>
インターフェイスを用いて、Observerパターンを実装してみます。
##Observerを実装する
まずはObserver(通知を受け取るクラス)を作成します。
###1.クラスを作成する
public class Observer
{
}
###2.IObserver<T>
インターフェイスを実装する
このクラスはObserverなので、IObserver<T>
インターフェイスを実装します。
型引数T
には受信したい値の型を指定します。ここでは、int
型とします。
public class Observer : IObserver<int>
{
public void OnCompleted()
{
throw new NotImplementedException();
}
public void OnError(Exception error)
{
throw new NotImplementedException();
}
public void OnNext(int value)
{
throw new NotImplementedException();
}
}
###3.値を受け取ったときのコールバック処理を記述する
あとは、各メソッドにそれぞれ通知が来たときに実行したい処理を自由に記述します。
例なので、ここでは次のようにコンソールにメッセージを出力するだけの処理を実装しました。
public class Observer : IObserver<int>
{
public void OnCompleted()
{
Console.WriteLine($"通知の受け取りが完了しました");
}
public void OnError(Exception error)
{
Console.WriteLine($"次のエラーを受信しました:{error.Message}");
}
public void OnNext(int value)
{
Console.WriteLine($"{value}を受け取りました");
}
}
これだけでObserverの実装は完了です。
###複数のObserverを用意できるようにする
冒頭にも書きましたが、Observerパターンでは、発行された値を複数のクラス(Observer)が同時に受け取ることができます。
これを試すために、次のようにObserverの名前をコンストラクタで指定するようにして、どのObserverがメッセージを受け取ったかを識別できるようにしておきます。
public class Observer : IObserver<int>
{
private string m_name;
public Observer(string name)
{
m_name = name;
}
public void OnCompleted()
{
Console.WriteLine($"{m_name}が通知の受け取りを完了しました");
}
public void OnError(Exception error)
{
Console.WriteLine($"{m_name}が次のエラーを受信しました:{error.Message}");
}
public void OnNext(int value)
{
Console.WriteLine($"{m_name}が{value}を受け取りました");
}
}
##Observableを実装する
次にObservable(通知を発行するクラス)を作成します。
###1.クラスを作成する
public class Observable
{
}
###2.IObservable<T>
インターフェイスを実装する
このクラスはObservableなので、IObservable<T>
インターフェイスを実装します。
型引数T
には発行したい値の型を指定します。ObserverとObservableの型引数は合致している必要があるため、ここでもT
はint
とします。
public class Observable : IObservable<int>
{
public IDisposable Subscribe(IObserver<int> observer)
{
throw new NotImplementedException();
}
}
###3.値の発行先を覚えておく仕組みを作成する
Observableの役目は、値の発行先として登録された(購読された、Subscribe
された)IObserver<T>
を記憶しておき、発行する値が生じたときに、そのすべてのIObserver<T>
に対して値を発行することです。
※「値を発行する」とは、具体的にはIObserver<T>
のOnNext
、OnCompleted
、もしくはOnError
メソッドを呼び出すことです
したがって、ObservableなクラスにSubscribe
で指定されたIObserver<int>
を覚えておける仕組みを作成する必要があります。
とはいっても、単にList<IObserver<T>>
に溜めておくだけで十分です。
public class Observable : IObservable<int>
{
//購読されたIObserver<int>のリスト
private List<IObserver<int>> m_observers = new List<IObserver<int>>();
public IDisposable Subscribe(IObserver<int> observer)
{
if(!m_observers.Contains(observer))
m_observers.Add(observer);
}
}
これで、Subscribe
で指定されたIObserver<int>
を覚えておくことができるようになりました。
###4.購読解除用のIDisposable
なクラスを用意する
ところで、Subscribe
メソッドの戻り値はIDisposable
になっています。
今まで説明しませんでしたが、Observerパターンは、値の発行先として登録するSubscribeの対となる機能として、値の発行を停止してもらう「購読解除」も可能となっています。
Subscribe
メソッドの戻り値として返すIDisposable
は、Subscribe
したObserverが、「もう値はいらないです」と購読を解除するときに使用するものです。
ご存知の通り、IDisposable
インターフェイスの中身はDispose
メソッドただ一つのみであり、__購読を解除したいObserverはこのIDisposable
をDispose
することによって購読を解除__することができます。
したがって、Dispose
されたときに購読を解除する仕組みを作成する必要があります。
「購読を解除する」とは、Observableなクラス視点で言えば、値の発行先リストであるList<IObserver<T>>
から購読を解除したいIObserver<T>
をRemove
することに他なりません。
では、どのように実装すればよいでしょうか?
まず、IDisposable
インターフェイスを返さなければなりませんから、当然、IDisposable
インターフェイスを実装したクラスが必要になります。
そこで、以下のように「購読を解除する責務を持ったIDisposable
なクラス」を作成します。
class Unsubscriber : IDisposable
{
public void Dispose()
{
throw new NotImplementedException();
}
}
そして、このDispose
メソッドの中に、購読者リストからRemove
する処理を書けば完成です。
具体的には、発行先リストList<IObserver<T>>
とDispose
されたときにRemove
するターゲットとなるIObserver<T>
を、コンストラクタで引き渡して、以下のように実装します。
class Unsubscriber : IDisposable
{
//発行先リスト
private List<IObserver<int>> m_observers;
//DisposeされたときにRemoveするIObserver<int>
private IObserver<int> m_observer;
public Unsubscriber(List<IObserver<int>> observers, IObserver<int> observer)
{
m_observers = observers;
m_observer = observer;
}
public void Dispose()
{
//Disposeされたら発行先リストから対象の発行先を削除する
m_observers.Remove(m_observer);
}
}
そして、この新しく作った購読解除用のIDisposable
なクラスのインスタンスを、Subscribe
の戻り値として返します。
public class Observable : IObservable<int>
{
//購読されたIObserver<int>のリスト
private List<IObserver<int>> m_observers = new List<IObserver<int>>();
public IDisposable Subscribe(IObserver<int> observer)
{
if(!m_observers.Contains(observer))
m_observers.Add(observer);
//購読解除用のクラスをIDisposableとして返す
return new Unsubscriber(m_observers, observer);
}
}
こうすることで、このIDisposable
を受け取ったObserverは、値が要らなくなった時点でDispose
メソッドを呼び出すことで、自分自身を発行先リストから削除することができるようになります。
ところで、このUnsubscriber
クラスは、Observable
クラス以外から生成されることはありません。
したがって、下記のようにObservable
クラスの内部クラスにしてしまいます。
public class Observable : IObservable<int>
{
//購読されたIObserver<int>のリスト
private List<IObserver<int>> m_observers = new List<IObserver<int>>();
public IDisposable Subscribe(IObserver<int> observer)
{
if(!m_observers.Contains(observer))
m_observers.Add(observer);
//購読解除用のクラスをIDisposableとして返す
return new Unsubscriber(m_observers, observer);
}
//購読解除用内部クラス
private class Unsubscriber : IDisposable
{
//発行先リスト
private List<IObserver<int>> m_observers;
//DisposeされたときにRemoveするIObserver<int>
private IObserver<int> m_observer;
public Unsubscriber(List<IObserver<int>> observers, IObserver<int> observer)
{
m_observers = observers;
m_observer = observer;
}
public void Dispose()
{
//Disposeされたら発行先リストから対象の発行先を削除する
m_observers.Remove(m_observer);
}
}
}
###5.通知を発行する処理を記述する
最後に、発行したい情報や値があった場合に通知を発行する処理を記述します。
ここでは例として、SendNotice
メソッドが呼ばれたときに“int”型の1,2,3を連続で通知を発行するようにします。
値を発行するには、OnNext
メソッドを呼び出します。
すべての発行先に対してOnNext
メソッドを呼び出すので、以下のようにforeach
を使用すればOKです。
public class Observable : IObservable<int>
{
//購読されたIObserver<int>のリスト
private List<IObserver<int>> m_observers = new List<IObserver<int>>();
public IDisposable Subscribe(IObserver<int> observer)
{
if(!m_observers.Contains(observer))
m_observers.Add(observer);
//購読解除用のクラスをIDisposableとして返す
return new Unsubscriber(m_observers, observer);
}
public void SendNotice()
{
//すべての発行先に対して1,2,3を発行する
foreach (var observer in m_observers)
{
observer.OnNext(1);
observer.OnNext(2);
observer.OnNext(3);
}
}
//購読解除用内部クラス
private class Unsubscriber : IDisposable
{
//発行先リスト
private List<IObserver<int>> m_observers;
//DisposeされたときにRemoveするIObserver<int>
private IObserver<int> m_observer;
public Unsubscriber(List<IObserver<int>> observers, IObserver<int> observer)
{
m_observers = observers;
m_observer = observer;
}
public void Dispose()
{
//Disposeされたら発行先リストから対象の発行先を削除する
m_observers.Remove(m_observer);
}
}
}
##使ってみる
これまで作成したObserverとObservableを使用して、実際に値の購読と発行、受け取りの流れを実演します。
ここでは、以下のようなコンソールアプリケーションを作成しました。
class Program
{
static void Main(string[] args)
{
//値を受け取るクラスを3つ作成
Observer observerA = new Observer("Aさん");
Observer observerB = new Observer("Bさん");
Observer observerC = new Observer("Cさん");
//値を発行するクラスを作成
Observable observable = new Observable();
//3つのObserverが、自分自身を発行先として登録する(=購読)
IDisposable disposableA = observable.Subscribe(observerA);
IDisposable disposableB = observable.Subscribe(observerB);
IDisposable disposableC = observable.Subscribe(observerC);
Console.WriteLine("Aさん〜Cさんが値を購読しました");
Console.WriteLine("値を発行させます");
//Observableに値を発行させる
observable.SendNotice();
Console.WriteLine("Aさんが購読解除します");
//Aさんが購読解除する
disposableA.Dispose();
Console.WriteLine("値を発行させます");
//再び値を発行させる
observable.SendNotice();
Console.WriteLine("Bさんが購読解除します");
//Bさんが購読解除する
disposableB.Dispose();
Console.WriteLine("値を発行させます");
//再び値を発行させる
observable.SendNotice();
Console.ReadKey();
}
}
やっている内容としては、コードを見たとおりですが
- 値の購読者(Observer)をAさん、Bさん、Cさんの3つを作成
- 値の発行者(Observable)を作成
- Aさん〜CさんがObservableを購読
- Observableに値を発行させる
- Aさん、購読を解除する
- Observableに再び値を発行させる
- Bさんも購読を解除する
- Observableに再び値を発行させる
となります。
これを実行すると、次のような出力が得られます。
Aさん〜Cさんが値を購読しました
値を発行させます
Aさんが1を受け取りました
Aさんが2を受け取りました
Aさんが3を受け取りました
Bさんが1を受け取りました
Bさんが2を受け取りました
Bさんが3を受け取りました
Cさんが1を受け取りました
Cさんが2を受け取りました
Cさんが3を受け取りました
Aさんが購読解除します
値を発行させます
Bさんが1を受け取りました
Bさんが2を受け取りました
Bさんが3を受け取りました
Cさんが1を受け取りました
Cさんが2を受け取りました
Cさんが3を受け取りました
Bさんが購読解除します
値を発行させます
Cさんが1を受け取りました
Cさんが2を受け取りました
Cさんが3を受け取りました
Observerパターンが期待通りに動作していることが確認できます。
#さいごに
以上、少し長くなってしまいましたがObserverパターンの解説を行いました。
僕自身としては、Observerパターンをこのようにそのまま利用することは少なく、ReactiveExtensionsというObserverパターンをベースとした非常に強力なライブラリを通じて利用することがほとんどです。
もともとは、このReactiveExtensionsの記事を書いていたのですが、ベースとなるObserverパターンの説明がとても長くなってしまったため、別記事として抜き出しました。
またReactiveExtensionsの記事も書いたらリンクを貼るのでそちらも良ければ御覧ください。
[2020/12/22追記]
ReactiveExtensionに関する記事書きました。よろしければ参照ください。
【C#】なぜReactiveExtensionsを導入すると幸せになれるのか
何か間違いやご指摘、ご質問あればコメントにお願いします。