この記事では、.NET C# のオブザーバー デザイン パターンといくつかの機能強化について説明します。
オブザーバーの設計パターンの定義
Observer デザイン パターンは、最も重要で一般的に使用されるデザイン パターンの 1 つです。
まず、 Observer Design Patternの正式な定義を確認しましょう。
オブザーバー デザイン パターンを使用すると、サブスクライバーはプロバイダーに登録して、プロバイダーからの通知を受け取ることができます。プッシュベースの通知を必要とするあらゆるシナリオに適しています。このパターンは、プロバイダー (サブジェクトまたはオブザーバブルとも呼ばれます) と、ゼロ、1 つ、または複数のオブザーバーを定義します。オブザーバーはプロバイダーに登録され、定義済みの条件、イベント、または状態の変化が発生するたびに、プロバイダーはメソッドの 1 つを呼び出して、すべてのオブザーバーに自動的に通知します。このメソッド呼び出しでは、プロバイダーはオブザーバーに現在の状態情報を提供することもできます。 .NET では、オブザーバー デザイン パターンは、汎用のSystem.IObservable<T>およびSystem.IObserver<T>インターフェイスを実装することによって適用されます。ジェネリック型パラメーターは、通知情報を提供する型を表します。
したがって、上記の定義から、次のことが理解できます。
- 2 つのパーティまたはモジュールがあります。
- 提供する情報のストリームを持つモジュール。このモジュールは、 Provider (情報を提供するため)、 Subject (情報を外部に公開するため)、またはObservable (外部から監視できるため) と呼ばれます。
- 他の場所からの情報の流れに関心を持つモジュール。このモジュールはObserverと呼ばれます (情報を監視するため)。
オブザーバー デザイン パターンの利点
現在わかっているように、 Observer Design Pattern は、 ObservableモジュールとObserverモジュールの間の関係を定式化します。オブザーバー デザイン パターンのユニークな点は、それを使用すると、密結合関係を持たなくてもこれを実現できることです。
パターンの仕組みを分析すると、次のことがわかります。
- Observable は、 Observerについて必要な最小限の情報を知っています。
- Observer は、 Observableについて必要な最小限の情報を知っています。
- 相互認識でさえ、具体的な実装ではなく、抽象化によって達成されます。
- 最後に、両方のモジュールがそれぞれの仕事を行うことができ、それぞれの仕事だけを行うことができます。
使用される抽象化
これらは、 .NET C#でObserver デザイン パターンを実装するために使用される抽象化です。
IObservable<out T>
これは、任意のObservableを表すCovariantインターフェースです。 .NET の Variance について詳しく知りたい場合は、こちらの記事をご覧ください。
このインターフェイスで定義されているメンバーは次のとおりです。
public IDisposable Subscribe (IObserver<out T> observer);
Subscribe
メソッドを呼び出して、 Observer がその情報ストリームに関心を持っていることをObservableに通知する必要があります。
Subscribe
メソッドは、 IDisposable
インターフェイスを実装するオブジェクトを返します。その後、このオブジェクトをObserverが使用して、 Observableによって提供される情報のストリームからサブスクライブを解除できます。これが完了すると、オブザーバーは情報のストリームに対する更新について通知されなくなります。
IObserver<in T>
これは、任意のObserverを表すContravariantインターフェースです。 .NET の Variance について詳しく知りたい場合は、こちらの記事をご覧ください。
このインターフェイスで定義されているメンバーは次のとおりです。
public void OnCompleted (); public void OnError (Exception error); public void OnNext (T value);
OnCompleted
メソッドは、 Observableによって呼び出され、情報のストリームが完了し、 Observer がそれ以上の情報を期待しないことをObserverに通知する必要があります。
OnError
メソッドは、 Observableによって呼び出され、 Observerにエラーが発生したことを通知する必要があります。
OnNext
メソッドは、 Observableによって呼び出され、新しい情報の準備ができてストリームに追加されていることをObserverに通知する必要があります。
マイクロソフトの実装
それでは、 Microsoft がObserver デザイン パターンをC# で実装することをどのように推奨しているかを見てみましょう。後で、私が自分で実装したマイナーな拡張機能をいくつか紹介します。
簡単な天気予報コンソール アプリケーションを作成します。このアプリケーションには、 WeatherForecastモジュール (Observable、Provider、Subject) とWeatherForecastObserverモジュール (Observer) があります。
それでは、実装の検討を始めましょう。
ウェザーインフォ
namespace Observable { public class WeatherInfo { internal WeatherInfo(double temperature) { Temperature = temperature; } public double Temperature { get; } } }
これは、情報ストリームに流れる情報を表すエンティティです。
天気予報
using System; using System.Collections.Generic; namespace Observable { public class WeatherForecast : IObservable<WeatherInfo> { private readonly List<IObserver<WeatherInfo>> m_Observers; private readonly List<WeatherInfo> m_WeatherInfoList; public WeatherForecast() { m_Observers = new List<IObserver<WeatherInfo>>(); m_WeatherInfoList = new List<WeatherInfo>(); } public IDisposable Subscribe(IObserver<WeatherInfo> observer) { if (!m_Observers.Contains(observer)) { m_Observers.Add(observer); foreach (var item in m_WeatherInfoList) { observer.OnNext(item); } } return new WeatherForecastUnsubscriber(m_Observers, observer); } public void RegisterWeatherInfo(WeatherInfo weatherInfo) { m_WeatherInfoList.Add(weatherInfo); foreach (var observer in m_Observers) { observer.OnNext(weatherInfo); } } public void ClearWeatherInfo() { m_WeatherInfoList.Clear(); } } }
ここで気付くこと:
-
WeatherForecast
クラスはIObservable<WeatherInfo>
を実装しています。 -
Subscribe
メソッドの実装では、渡された Observer が以前に登録されていたかどうかを確認します。そうでない場合は、ローカルのm_Observers
オブザーバー リストに追加します。次に、ローカルのm_WeatherInfoList
リストにあるすべてのWeatherInfo
エントリを 1 つずつループし、Observer のOnNext
メソッドを呼び出して Observer に通知します。 - 最後に、情報ストリームからの登録解除のために Observer が使用する
WeatherForecastUnsubscriber
クラスの新しいインスタンスを返します。 -
RegisterWeatherInfo
メソッドは、メイン モジュールが新しいWeatherInfo
を登録できるように定義されています。現実の世界では、これは内部のスケジュールされたAPI 呼び出し、またはSignalR Hubへのリスナー、または情報源として機能するその他のものに置き換えることができます。
Unsubscriber<T>
using System; using System.Collections.Generic; namespace Observable { public class Unsubscriber<T> : IDisposable { private readonly List<IObserver<T>> m_Observers; private readonly IObserver<T> m_Observer; private bool m_IsDisposed; public Unsubscriber(List<IObserver<T>> observers, IObserver<T> observer) { m_Observers = observers; m_Observer = observer; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (m_IsDisposed) return; if (disposing && m_Observers.Contains(m_Observer)) { m_Observers.Remove(m_Observer); } m_IsDisposed = true; } ~Unsubscriber() { Dispose(false); } } }
ここで気付くこと:
- これは、Un-subscriber の基本クラスです。
- Disposable Design Patternを適用して
IDisposable
を実装します。 - コンストラクターを介して、オブザーバーの完全なリストと、それが作成されたオブザーバーを受け取ります。
- 廃棄中に、オブザーバーがオブザーバーの完全なリストに既に存在するかどうかを確認します。はいの場合、リストから削除します。
天気予報購読解除
using System; using System.Collections.Generic; namespace Observable { public class WeatherForecastUnsubscriber : Unsubscriber<WeatherInfo> { public WeatherForecastUnsubscriber( List<IObserver<WeatherInfo>> observers, IObserver<WeatherInfo> observer) : base(observers, observer) { } } }
ここで気付くこと:
- これは
Unsubscriber<T>
クラスから継承しています。 - 特別な取り扱いはありません。
WeatherForecastObserver
using System; namespace Observable { public class WeatherForecastObserver : IObserver<WeatherInfo> { private IDisposable m_Unsubscriber; public virtual void Subscribe(WeatherForecast provider) { m_Unsubscriber = provider.Subscribe(this); } public virtual void Unsubscribe() { m_Unsubscriber.Dispose(); } public void OnCompleted() { Console.WriteLine("Completed"); } public void OnError(Exception error) { Console.WriteLine("Error"); } public void OnNext(WeatherInfo value) { Console.WriteLine($"Temperature: {value.Temperature}"); } } }
ここで気付くこと:
-
WeatherForecastObserver
クラスはIObserver<WeatherInfo>
を実装しています。 -
OnNext
メソッドでは、温度をコンソールに書き込みます。 -
OnCompleted
メソッドでは、「完了」をコンソールに書き込みます。 -
OnError
メソッドでは、「エラー」をコンソールに書き込みます。 - メインモジュールが登録プロセスをトリガーできるように、
void Subscribe(WeatherForecast provider)
メソッドを定義しました。返されたサブスクライバー解除オブジェクトは、サブスクライブ解除の場合に使用するために内部的に保存されます。 - 同じ概念を使用して、
void Unsubscribe()
メソッドが定義され、内部的に保存されたサブスクライバー解除オブジェクトを利用します。
プログラム
using System; namespace Observable { class Program { static void Main(string[] args) { var provider = new WeatherForecast(); provider.RegisterWeatherInfo(new WeatherInfo(1)); provider.RegisterWeatherInfo(new WeatherInfo(2)); provider.RegisterWeatherInfo(new WeatherInfo(3)); var observer = new WeatherForecastObserver(); observer.Subscribe(provider); provider.RegisterWeatherInfo(new WeatherInfo(4)); provider.RegisterWeatherInfo(new WeatherInfo(5)); observer.Unsubscribe(); provider.RegisterWeatherInfo(new WeatherInfo(6)); observer.Subscribe(provider); provider.RegisterWeatherInfo(new WeatherInfo(7)); Console.ReadLine(); } } }
ここで気付くこと:
- プロバイダーのインスタンスを作成しました。
- 次に、3つの情報を登録しました。
- この時点まで、オブザーバーが定義されていないため、コンソールには何も記録されません。
- 次に、オブザーバーのインスタンスを作成しました。
- 次に、オブザーバーをストリームにサブスクライブしました。
- この時点で、コンソールに記録された 3 つの温度が表示されます。これは、オブザーバーがサブスクライブすると、既存の情報について通知されるためです。私たちの場合、それらは 3 つの情報です。
- 次に、2 つの情報を登録します。
- そのため、さらに 2 つのメッセージがコンソールに記録されます。
- 次に、購読を解除します。
- 次に、1 つの情報を登録します。
- ただし、オブザーバーが既にサブスクライブを解除しているため、この情報はコンソールに記録されません。
- その後、オブザーバーは再びサブスクライブします。
- 次に、1 つの情報を登録します。
- したがって、この情報はコンソールに記録されます。
最後に、これを実行すると、次の結果になります。
私の拡張実装
Microsoft の実装を確認したところ、いくつかの懸念事項が見つかりました。そのため、いくつかの小さな変更を行うことにしました。
IExtendedObservable<out T>
using System; using System.Collections.Generic; namespace ExtendedObservable { public interface IExtendedObservable<out T> : IObservable<T> { IReadOnlyCollection<T> Snapshot { get; } IDisposable Subscribe(IObserver<T> observer, bool withHistory); } }
ここで気付くこと:
-
IExtendedObservable<out T>
インターフェイスは、IObservable<T>
インターフェイスを拡張します。 - 共変です。詳しく知りたい方はこちらの記事もチェック
.NET C# の共分散と反分散 . -
IReadOnlyCollection<T> Snapshot
プロパティを定義して、他のモジュールがサブスクライブしなくても既存の情報エントリのインスタント リストを取得できるようにしました。 - また、追加の
bool withHistory
パラメーターを使用してIDisposable Subscribe(IObserver<T> observer, bool withHistory)
メソッドを定義し、オブザーバーがサブスクライブ時に既存の情報エントリについて通知を受けるかどうかを決定できるようにしました。
退会者
using System; namespace ExtendedObservable { public class Unsubscriber : IDisposable { private readonly Action m_UnsubscribeAction; private bool m_IsDisposed; public Unsubscriber(Action unsubscribeAction) { m_UnsubscribeAction = unsubscribeAction; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (m_IsDisposed) return; if (disposing) { m_UnsubscribeAction(); } m_IsDisposed = true; } ~Unsubscriber() { Dispose(false); } } }
ここで気付くこと:
- 現在、
Unsubscriber
クラスはジェネリックではありません。 - これは、情報エンティティの型を知る必要がなくなったためです。
- オブザーバーの完全なリストとそれが作成されたオブザーバーにアクセスする代わりに、オブザーバブルが破棄されたときにオブザーバブルに通知し、オブザーバブルが登録解除プロセスを単独で処理します。
- このように、それは以前よりも少なくなり、その仕事をしているだけです.
天気予報購読解除
using System; using System.Collections.Generic; namespace ExtendedObservable { public class WeatherForecastUnsubscriber : Unsubscriber { public WeatherForecastUnsubscriber( Action unsubscribeAction) : base(unsubscribeAction) { } } }
ここで気付くこと:
-
Unsubscriber<T>
から<T>
部分を削除しました。 - そして今、コンストラクターは、破棄の場合に呼び出される
Action
を受け取ります。
天気予報
using System; using System.Collections.Generic; namespace ExtendedObservable { public class WeatherForecast : IExtendedObservable<WeatherInfo> { private readonly List<IObserver<WeatherInfo>> m_Observers; private readonly List<WeatherInfo> m_WeatherInfoList; public WeatherForecast() { m_Observers = new List<IObserver<WeatherInfo>>(); m_WeatherInfoList = new List<WeatherInfo>(); } public IReadOnlyCollection<WeatherInfo> Snapshot => m_WeatherInfoList; public IDisposable Subscribe(IObserver<WeatherInfo> observer) { return Subscribe(observer, false); } public IDisposable Subscribe(IObserver<WeatherInfo> observer, bool withHistory) { if (!m_Observers.Contains(observer)) { m_Observers.Add(observer); if (withHistory) { foreach (var item in m_WeatherInfoList) { observer.OnNext(item); } } } return new WeatherForecastUnsubscriber( () => { if (m_Observers.Contains(observer)) { m_Observers.Remove(observer); } }); } public void RegisterWeatherInfo(WeatherInfo weatherInfo) { m_WeatherInfoList.Add(weatherInfo); foreach (var observer in m_Observers) { observer.OnNext(weatherInfo); } } public void ClearWeatherInfo() { m_WeatherInfoList.Clear(); } } }
ここで気付くこと:
- 内部の
m_WeatherInfoList
リストを返すIReadOnlyCollection<WeatherInfo> Snapshot
プロパティ以外はほとんど同じですが、IReadOnlyCollection
と同じです。 - そして
withHistory
パラメータを利用するIDisposable Subscribe(IObserver<WeatherInfo> observer, bool withHistory)
メソッド。
WeatherForecastObserver
using System; namespace ExtendedObservable { public class WeatherForecastObserver : IObserver<WeatherInfo> { private IDisposable m_Unsubscriber; public virtual void Subscribe(WeatherForecast provider) { m_Unsubscriber = provider.Subscribe(this, true); } public virtual void Unsubscribe() { m_Unsubscriber.Dispose(); } public void OnCompleted() { Console.WriteLine("Completed"); } public void OnError(Exception error) { Console.WriteLine("Error"); } public void OnNext(WeatherInfo value) { Console.WriteLine($"Temperature: {value.Temperature}"); } } }
ここで気付くのは、 Subscribe(WeatherForecast provider)
を除いてほぼ同じであり、履歴を使用してSubscribe
するかどうかを決定することです。
プログラム
using System; namespace ExtendedObservable { class Program { static void Main(string[] args) { var provider = new WeatherForecast(); provider.RegisterWeatherInfo(new WeatherInfo(1)); provider.RegisterWeatherInfo(new WeatherInfo(2)); provider.RegisterWeatherInfo(new WeatherInfo(3)); var observer = new WeatherForecastObserver(); observer.Subscribe(provider); provider.RegisterWeatherInfo(new WeatherInfo(4)); provider.RegisterWeatherInfo(new WeatherInfo(5)); observer.Unsubscribe(); provider.RegisterWeatherInfo(new WeatherInfo(6)); observer.Subscribe(provider); provider.RegisterWeatherInfo(new WeatherInfo(7)); Console.ReadLine(); } } }
以前と同じです。
最後に、これを実行すると、以前と同じ結果になるはずです。
次は何ですか
これで、.NET C# のオブザーバー デザイン パターンの基本を理解できました。しかし、これで話は終わりではありません。
IObservable<T>
およびIObserver<T>
インターフェイスの上に構築されたライブラリがあり、便利な機能を提供します。
これらのライブラリの 1 つは、
したがって、これらのライブラリを調べて試してみることをお勧めします。私はあなたがそれらのいくつかを好きになると確信しています.