この記事では、.NET C# のオブザーバー デザイン パターンといくつかの機能強化について説明します。
Observer デザイン パターンは、最も重要で一般的に使用されるデザイン パターンの 1 つです。
まず、 Observer Design Patternの正式な定義を確認しましょう。
オブザーバー デザイン パターンを使用すると、サブスクライバーはプロバイダーに登録して、プロバイダーからの通知を受け取ることができます。プッシュベースの通知を必要とするあらゆるシナリオに適しています。このパターンは、プロバイダー (サブジェクトまたはオブザーバブルとも呼ばれます) と、ゼロ、1 つ、または複数のオブザーバーを定義します。オブザーバーはプロバイダーに登録され、定義済みの条件、イベント、または状態の変化が発生するたびに、プロバイダーはメソッドの 1 つを呼び出して、すべてのオブザーバーに自動的に通知します。このメソッド呼び出しでは、プロバイダーはオブザーバーに現在の状態情報を提供することもできます。 .NET では、オブザーバー デザイン パターンは、汎用のSystem.IObservable<T>およびSystem.IObserver<T>インターフェイスを実装することによって適用されます。ジェネリック型パラメーターは、通知情報を提供する型を表します。
現在わかっているように、 Observer Design Pattern は、 ObservableモジュールとObserverモジュールの間の関係を定式化します。オブザーバー デザイン パターンのユニークな点は、それを使用すると、密結合関係を持たなくてもこれを実現できることです。
パターンの仕組みを分析すると、次のことがわかります。
これらは、 .NET C#でObserver デザイン パターンを実装するために使用される抽象化です。
これは、任意のObservableを表すCovariantインターフェースです。 .NET の Variance について詳しく知りたい場合は、こちらの記事をご覧ください。
このインターフェイスで定義されているメンバーは次のとおりです。
public IDisposable Subscribe (IObserver<out T> observer);
Subscribe
メソッドを呼び出して、 Observer がその情報ストリームに関心を持っていることをObservableに通知する必要があります。
Subscribe
メソッドは、 IDisposable
インターフェイスを実装するオブジェクトを返します。その後、このオブジェクトをObserverが使用して、 Observableによって提供される情報のストリームからサブスクライブを解除できます。これが完了すると、オブザーバーは情報のストリームに対する更新について通知されなくなります。
これは、任意の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 に通知します。WeatherForecastUnsubscriber
クラスの新しいインスタンスを返します。RegisterWeatherInfo
メソッドは、メイン モジュールが新しいWeatherInfo
を登録できるように定義されています。現実の世界では、これは内部のスケジュールされたAPI 呼び出し、またはSignalR Hubへのリスナー、または情報源として機能するその他のものに置き換えることができます。
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); } } }
ここで気付くこと:
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>
クラスから継承しています。
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(); } } }
ここで気付くこと:
Microsoft の実装を確認したところ、いくつかの懸念事項が見つかりました。そのため、いくつかの小さな変更を行うことにしました。
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>
インターフェイスを拡張します。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)
メソッド。
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 つは、
したがって、これらのライブラリを調べて試してみることをお勧めします。私はあなたがそれらのいくつかを好きになると確信しています.