オブザーバー 設計パターンのベスト プラクティス

.NET では、オブザーバー デザイン パターンはインターフェイスのセットとして実装されます。 System.IObservable<T> インターフェイスはデータ プロバイダーを表します。これは、オブザーバーが通知のサブスクライブを解除できるようにするIDisposable実装を提供する役割も担います。 System.IObserver<T> インターフェイスはオブザーバーを表します。

この記事では、これらのインターフェイスを使用してオブザーバー 設計パターンを実装するときに従うベスト プラクティスについて説明します。

実装する前に代替手段を検討する

IObservable<T> インターフェイスと IObserver<T> インターフェイスはプッシュベースの通知シナリオに適していますが、他の.NET パターンの方が適している可能性があります。 1 つのアプリケーション内で簡単な通知を行うには、イベントを使用 します。 コンシューマーがペースを制御する非同期プルベースのシーケンスの場合は、 IAsyncEnumerable<T>を使用します。 バックプレッシャのあるプロデューサー/コンシューマー パターンの場合は、 System.Threading.Channelsを使用します。 複雑なイベントの構成、フィルター処理、変換の場合は、System.Reactive を直接実装する代わりに、IObservable<T> (Rx.NET) パッケージを使用します。 詳細については、「 オブザーバーデザインパターン」を参照してください。

通知データに別の種類を使用する

プロバイダーがオブザーバーに送信するデータを含むオブジェクトは、 IObservable<T> および IObserver<T>のジェネリック型パラメーターに対応します。 このオブジェクトは IObservable<T> 実装と同じにすることができますが、別の型として定義します。 専用のデータ型は、通知ペイロードとは別にプロバイダーの責任を維持し、API の進化を容易にします。

通知の順序に依存しない

オブザーバーが通知を受信する順序は定義されていません。 プロバイダーは任意のメソッドを自由に使用して順序を決定できるため、別のオブザーバーの前または後に通知されることに依存するオブザーバーを記述しないでください。

Subscribe と Dispose をスレッド セーフにする

通常、プロバイダーは、コレクション オブジェクトで表されるサブスクライバー リストにオブザーバーを追加して IObservable<T>.Subscribe メソッドを実装し、そのリストからオブザーバーを削除することによって IDisposable.Dispose メソッドを実装します。 オブザーバーは、いつでもこれらのメソッドを呼び出すことができます。 プロバイダー/オブザーバー コントラクトでは、 IObserver<T>.OnCompleted コールバック メソッドの後にサブスクライブ解除を担当するユーザーが指定されていないため、プロバイダーとオブザーバーの両方が同じメンバーをリストから削除しようとする可能性があります。

競合状態を回避するには、 Subscribe メソッドと Dispose メソッドの両方をスレッド セーフにします。 通常、これには 同時実行コレクション またはロックの使用が含まれます。 スレッド セーフではない実装では、そうでないことを明示的に文書化する必要があります。

追加契約の保証を文書化する

プロバイダー/オブザーバー コントラクトの上にあるレイヤーで、追加の保証を指定します。 他の要件を課す場合は、オブザーバー コントラクトについてユーザーが混乱しないように、それらを明確に呼び出します。

例外を情報として処理する

データ プロバイダーとオブザーバーの間の疎結合のため、オブザーバー デザイン パターンの例外は情報提供を目的としています。 この特性は、プロバイダーとオブザーバーが例外を処理する方法に影響します。

更新を続行できない場合にのみ OnError を呼び出す

OnErrorメソッドは、IObserver<T>.OnNext メソッドと同様に、オブザーバーへの情報メッセージとして意図されています。 ただし、 OnNext メソッドは現在または更新されたデータをオブザーバーに提供しますが、 OnError メソッドはプロバイダーが有効なデータを提供できないことを示します。

例外を処理し、 OnError メソッドを呼び出すときは、次のベスト プラクティスに従います。

  • 特定の要件がある場合、プロバイダーは独自の例外を処理する必要があります。
  • プロバイダーは、オブザーバーが特定の方法で例外を処理することを想定したり、要求したりしないでください。
  • プロバイダーは、更新プログラムを提供する機能を損なう例外を処理するときに、 OnError メソッドを呼び出す必要があります。 このような例外に関する情報をオブザーバーに渡します。 それ以外の場合は、オブザーバーに例外を通知する必要はありません。

プロバイダーが OnError メソッドまたは IObserver<T>.OnCompleted メソッドを呼び出した後は、それ以上の通知はなく、プロバイダーはオブザーバーの登録を解除できます。 ただし、オブザーバーは、 OnError または IObserver<T>.OnCompleted 通知を受け取る前後を含め、いつでもサブスクリプションを解除することもできます。 オブザーバーの設計パターンでは、プロバイダーとオブザーバーのどちらがサブスクリプション解除の責任を負うかは決まらないので、どちらもサブスクライブ解除を試みる可能性があります。 通常、オブザーバーがサブスクリプションを解除すると、サブスクライバー コレクションから削除されます。 シングル スレッド アプリケーションでは、 IDisposable.Dispose 実装では、オブジェクト参照が有効であること、およびオブジェクトがサブスクライバー コレクションのメンバーであることを確認してから、オブジェクトを削除する必要があります。 マルチスレッド アプリケーションでは、ロックを使用してオブザーバー コレクションを保護します。

オブザーバーにおいて OnError 通知を情報として扱う

オブザーバーがプロバイダーからエラー通知を受信すると、オブザーバーは例外を情報として扱う必要があり、特定のアクションを実行する必要はありません。

プロバイダーからの OnError メソッド呼び出しに応答するときは、次のベスト プラクティスに従います。

  • OnNextOnErrorなどのインターフェイス実装から例外をスローしないでください。 オブザーバーが実際に例外をスローした場合、これらの例外は未処理のままになるものと考えてください。
  • コール スタックを保持するには、OnError メソッドに渡された Exception オブジェクトをスローしようとするオブザーバーは、そのオブジェクトをスローする前に例外でラップする必要があります。 この目的には標準の例外オブジェクトを使用します。

Subscribe メソッドで登録を解除しない

IObservable<T>.Subscribe メソッドでは null 参照が発生する可能性があるため、登録解除を試みないでください。

オブザーバーを 1 つのプロバイダーにアタッチする

オブザーバーは複数のプロバイダーにアタッチできますが、推奨されるパターンは、 IObserver<T> インスタンスを 1 つの IObservable<T> インスタンスにのみアタッチすることです。