次の方法で共有


オブサーバー設計パターン

オブザーバー 設計パターンを使用すると、サブスクライバーはプロバイダーに登録し、プロバイダーから通知を受信できます。 これは、プッシュベースの通知を必要とする任意のシナリオに適しています。 このパターンでは、 プロバイダー ( サブジェクト または 観測可能とも呼ばれます) と、0 個、1 つ以上の オブザーバーを定義します。 オブザーバーはプロバイダーに登録し、定義済みの条件、イベント、または状態の変更が発生するたびに、プロバイダーはデリゲートを呼び出すことによってすべてのオブザーバーに自動的に通知します。 このメソッド呼び出しでは、プロバイダーはオブザーバーに現在の状態情報を提供することもできます。 .NET では、汎用の System.IObservable<T> インターフェイスと System.IObserver<T> インターフェイスを実装することで、オブザーバー デザイン パターンが適用されます。 ジェネリック型パラメーターは、通知情報を提供する型を表します。

パターンを適用するタイミング

オブザーバーの設計パターンは、データ ソース (ビジネス ロジック) レイヤーやユーザー インターフェイス (表示) レイヤーなど、2 つの異なるコンポーネントまたはアプリケーション レイヤー間のクリーンな分離をサポートするため、分散プッシュベースの通知に適しています。 このパターンは、プロバイダーがコールバックを使用してクライアントに現在の情報を提供するたびに実装できます。

パターンを実装するには、次の詳細を指定する必要があります。

  • プロバイダーまたは件名。オブザーバーに通知を送信するオブジェクトです。 プロバイダーは、 IObservable<T> インターフェイスを実装するクラスまたは構造体です。 プロバイダーは、プロバイダーから通知を受信するオブザーバーによって呼び出される 1 つのメソッド ( IObservable<T>.Subscribe) を実装する必要があります。

  • オブザーバー。プロバイダーから通知を受け取るオブジェクトです。 オブザーバーは、 IObserver<T> インターフェイスを実装するクラスまたは構造体です。 オブザーバーは、プロバイダーによって呼び出される 3 つのメソッドを実装する必要があります。

  • プロバイダーがオブザーバーを追跡できるようにするメカニズム。 通常、プロバイダーは、 System.Collections.Generic.List<T> オブジェクトなどのコンテナー オブジェクトを使用して、通知をサブスクライブした IObserver<T> 実装への参照を保持します。 この目的でストレージ コンテナーを使用すると、プロバイダーは 0 から無制限のオブザーバーを処理できます。 オブザーバーが通知を受信する順序は定義されていません。プロバイダーは、任意のメソッドを自由に使用して順序を決定できます。

  • 通知が完了したときにプロバイダーがオブザーバーを削除できるようにする IDisposable 実装。 オブザーバーは、IDisposable メソッドからSubscribe実装への参照を受け取るので、プロバイダーが通知の送信を完了する前に、IDisposable.Dispose メソッドを呼び出してサブスクライブを解除することもできます。

  • プロバイダーがオブザーバーに送信するデータを格納しているオブジェクト。 このオブジェクトの型は、 IObservable<T> インターフェイスと IObserver<T> インターフェイスのジェネリック型パラメーターに対応します。 このオブジェクトは IObservable<T> 実装と同じにすることができますが、通常は別の型です。

オブザーバー デザイン パターンの実装に加えて、 IObservable<T> インターフェイスと IObserver<T> インターフェイスを使用して構築されたライブラリの探索にも関心がある場合があります。 たとえば、 .NET (Rx) のリアクティブ拡張 は、一連の拡張メソッドと、非同期プログラミングをサポートする LINQ 標準シーケンス演算子で構成されます。

パターンを実装する

次の例では、オブザーバーの設計パターンを使用して、空港手荷物クレーム情報システムを実装します。 BaggageInfoクラスでは、到着する便とその便からの手荷物を受け取るカルーセルに関する情報を提供します。 次の例に示します。

namespace Observables.Example;

public readonly record struct BaggageInfo(
    int FlightNumber,
    string From,
    int Carousel);
Public Class BaggageInfo
    Private flightNo As Integer
    Private origin As String
    Private location As Integer

    Friend Sub New(ByVal flight As Integer, ByVal from As String, ByVal carousel As Integer)
        Me.flightNo = flight
        Me.origin = from
        Me.location = carousel
    End Sub

    Public ReadOnly Property FlightNumber As Integer
        Get
            Return Me.flightNo
        End Get
    End Property

    Public ReadOnly Property From As String
        Get
            Return Me.origin
        End Get
    End Property

    Public ReadOnly Property Carousel As Integer
        Get
            Return Me.location
        End Get
    End Property
End Class

BaggageHandlerクラスは、到着便と手荷物受取用カルーセルに関する情報を受け取る役割を担っています。 内部的には、次の 2 つのコレクションが保持されます。

  • _observers: 更新された情報を監視するクライアントのコレクション。
  • _flights: 飛行機の便と、各便に割り当てられた手荷物の受け取り場所のコレクション。

BaggageHandler クラスのソース コードを次の例に示します。

namespace Observables.Example;

public sealed class BaggageHandler : IObservable<BaggageInfo>
{
    private readonly HashSet<IObserver<BaggageInfo>> _observers = new();
    private readonly HashSet<BaggageInfo> _flights = new();

    public IDisposable Subscribe(IObserver<BaggageInfo> observer)
    {
        // Check whether observer is already registered. If not, add it.
        if (_observers.Add(observer))
        {
            // Provide observer with existing data.
            foreach (BaggageInfo item in _flights)
            {
                observer.OnNext(item);
            }
        }

        return new Unsubscriber<BaggageInfo>(_observers, observer);
    }

    // Called to indicate all baggage is now unloaded.
    public void BaggageStatus(int flightNumber) =>
        BaggageStatus(flightNumber, string.Empty, 0);

    public void BaggageStatus(int flightNumber, string from, int carousel)
    {
        var info = new BaggageInfo(flightNumber, from, carousel);

        // Carousel is assigned, so add new info object to list.
        if (carousel > 0 && _flights.Add(info))
        {
            foreach (IObserver<BaggageInfo> observer in _observers)
            {
                observer.OnNext(info);
            }
        }
        else if (carousel is 0)
        {
            // Baggage claim for flight is done.
            if (_flights.RemoveWhere(
                flight => flight.FlightNumber == info.FlightNumber) > 0)
            {
                foreach (IObserver<BaggageInfo> observer in _observers)
                {
                    observer.OnNext(info);
                }
            }
        }
    }

    public void LastBaggageClaimed()
    {
        foreach (IObserver<BaggageInfo> observer in _observers)
        {
            observer.OnCompleted();
        }

        _observers.Clear();
    }
}
Public Class BaggageHandler : Implements IObservable(Of BaggageInfo)

    Private observers As List(Of IObserver(Of BaggageInfo))
    Private flights As List(Of BaggageInfo)

    Public Sub New()
        observers = New List(Of IObserver(Of BaggageInfo))
        flights = New List(Of BaggageInfo)
    End Sub

    Public Function Subscribe(ByVal observer As IObserver(Of BaggageInfo)) As IDisposable _
                    Implements IObservable(Of BaggageInfo).Subscribe
        ' Check whether observer is already registered. If not, add it
        If Not observers.Contains(observer) Then
            observers.Add(observer)
            ' Provide observer with existing data.
            For Each item In flights
                observer.OnNext(item)
            Next
        End If
        Return New Unsubscriber(Of BaggageInfo)(observers, observer)
    End Function

    ' Called to indicate all baggage is now unloaded.
    Public Sub BaggageStatus(ByVal flightNo As Integer)
        BaggageStatus(flightNo, String.Empty, 0)
    End Sub

    Public Sub BaggageStatus(ByVal flightNo As Integer, ByVal from As String, ByVal carousel As Integer)
        Dim info As New BaggageInfo(flightNo, from, carousel)

        ' Carousel is assigned, so add new info object to list.
        If carousel > 0 And Not flights.Contains(info) Then
            flights.Add(info)
            For Each observer In observers
                observer.OnNext(info)
            Next
        ElseIf carousel = 0 Then
            ' Baggage claim for flight is done
            Dim flightsToRemove As New List(Of BaggageInfo)
            For Each flight In flights
                If info.FlightNumber = flight.FlightNumber Then
                    flightsToRemove.Add(flight)
                    For Each observer In observers
                        observer.OnNext(info)
                    Next
                End If
            Next
            For Each flightToRemove In flightsToRemove
                flights.Remove(flightToRemove)
            Next
            flightsToRemove.Clear()
        End If
    End Sub

    Public Sub LastBaggageClaimed()
        For Each observer In observers
            observer.OnCompleted()
        Next
        observers.Clear()
    End Sub
End Class

更新された情報を受信するクライアントは、 BaggageHandler.Subscribe メソッドを呼び出します。 クライアントが以前に通知をサブスクライブしていない場合は、クライアントの IObserver<T> 実装への参照が _observers コレクションに追加されます。

オーバーロードされた BaggageHandler.BaggageStatus メソッドを呼び出して、フライトからの手荷物がアンロードされているか、またはアンロードされていないことを示すことができます。 最初の場合、このメソッドにはフライト番号、フライトが出発した空港、荷物が降ろされているカルーセルが渡されます。 2 番目のケースでは、メソッドにはフライト番号のみが渡されます。 アンロード中の手荷物については、メソッドに渡された BaggageInfo 情報が _flights コレクションに存在するかどうかを確認します。 そうでない場合、メソッドは情報を追加し、各オブザーバーの OnNext メソッドを呼び出します。 手荷物がアンロードされなくなったフライトの場合、このメソッドは、そのフライトに関する情報が _flights コレクションに保存されているかどうかを確認します。 その場合、メソッドは各オブザーバーのOnNext メソッドを呼び出し、BaggageInfo コレクションから_flights オブジェクトを削除します。

その日の最後のフライトが着陸し、その手荷物が処理されると、 BaggageHandler.LastBaggageClaimed メソッドが呼び出されます。 このメソッドは、各オブザーバーの OnCompleted メソッドを呼び出して、すべての通知が完了したことを示し、 _observers コレクションをクリアします。

プロバイダーの Subscribe メソッドは、IDisposable メソッドが呼び出される前にオブザーバーが通知の受信を停止できるようにするOnCompleted実装を返します。 この Unsubscriber(Of BaggageInfo) クラスのソース コードを次の例に示します。 BaggageHandler.Subscribe メソッドでクラスがインスタンス化されると、_observers コレクションへの参照と、コレクションに追加されるオブザーバーへの参照が渡されます。 これらの参照はローカル変数に割り当てられます。 オブジェクトの Dispose メソッドが呼び出されると、 _observers コレクションにオブザーバーがまだ存在するかどうかを確認し、存在する場合はオブザーバーを削除します。

namespace Observables.Example;

internal sealed class Unsubscriber<BaggageInfo> : IDisposable
{
    private readonly ISet<IObserver<BaggageInfo>> _observers;
    private readonly IObserver<BaggageInfo> _observer;

    internal Unsubscriber(
        ISet<IObserver<BaggageInfo>> observers,
        IObserver<BaggageInfo> observer) => (_observers, _observer) = (observers, observer);

    public void Dispose() => _observers.Remove(_observer);
}
Friend Class Unsubscriber(Of BaggageInfo) : Implements IDisposable
    Private _observers As List(Of IObserver(Of BaggageInfo))
    Private _observer As IObserver(Of BaggageInfo)

    Friend Sub New(ByVal observers As List(Of IObserver(Of BaggageInfo)), ByVal observer As IObserver(Of BaggageInfo))
        Me._observers = observers
        Me._observer = observer
    End Sub

    Public Sub Dispose() Implements IDisposable.Dispose
        If _observers.Contains(_observer) Then
            _observers.Remove(_observer)
        End If
    End Sub
End Class

次の例では、IObserver<T>という名前のArrivalsMonitor実装を示します。これは、手荷物の請求情報を表示する基本クラスです。 情報は、発信都市の名前でアルファベット順に表示されます。 ArrivalsMonitorのメソッドはoverridable (Visual Basic の場合) または virtual (C# の場合) としてマークされるため、派生クラスでオーバーライドできます。

namespace Observables.Example;

public class ArrivalsMonitor : IObserver<BaggageInfo>
{
    private readonly string _name;
    private readonly List<string> _flights = new();
    private readonly string _format = "{0,-20} {1,5}  {2, 3}";
    private IDisposable? _cancellation;    

    public ArrivalsMonitor(string name)
    {
        ArgumentException.ThrowIfNullOrEmpty(name);
        _name = name;
    }

    public virtual void Subscribe(BaggageHandler provider) =>
        _cancellation = provider.Subscribe(this);

    public virtual void Unsubscribe()
    {
        _cancellation?.Dispose();
        _flights.Clear();
    }

    public virtual void OnCompleted() => _flights.Clear();

    // No implementation needed: Method is not called by the BaggageHandler class.
    public virtual void OnError(Exception e)
    {
        // No implementation.
    }

    // Update information.
    public virtual void OnNext(BaggageInfo info)
    {
        bool updated = false;

        // Flight has unloaded its baggage; remove from the monitor.
        if (info.Carousel is 0)
        {
            string flightNumber = string.Format("{0,5}", info.FlightNumber);
            for (int index = _flights.Count - 1; index >= 0; index--)
            {
                string flightInfo = _flights[index];
                if (flightInfo.Substring(21, 5).Equals(flightNumber))
                {
                    updated = true;
                    _flights.RemoveAt(index);
                }
            }
        }
        else
        {
            // Add flight if it doesn't exist in the collection.
            string flightInfo = string.Format(_format, info.From, info.FlightNumber, info.Carousel);
            if (_flights.Contains(flightInfo) is false)
            {
                _flights.Add(flightInfo);
                updated = true;
            }
        }

        if (updated)
        {
            _flights.Sort();
            Console.WriteLine($"Arrivals information from {_name}");
            foreach (string flightInfo in _flights)
            {
                Console.WriteLine(flightInfo);
            }

            Console.WriteLine();
        }
    }
}
Public Class ArrivalsMonitor : Implements IObserver(Of BaggageInfo)
    Private name As String
    Private flightInfos As New List(Of String)
    Private cancellation As IDisposable
    Private fmt As String = "{0,-20} {1,5}  {2, 3}"

    Public Sub New(ByVal name As String)
        If String.IsNullOrEmpty(name) Then Throw New ArgumentNullException("The observer must be assigned a name.")

        Me.name = name
    End Sub

    Public Overridable Sub Subscribe(ByVal provider As BaggageHandler)
        cancellation = provider.Subscribe(Me)
    End Sub

    Public Overridable Sub Unsubscribe()
        cancellation.Dispose()
        flightInfos.Clear()
    End Sub

    Public Overridable Sub OnCompleted() Implements System.IObserver(Of BaggageInfo).OnCompleted
        flightInfos.Clear()
    End Sub

    ' No implementation needed: Method is not called by the BaggageHandler class.
    Public Overridable Sub OnError(ByVal e As System.Exception) Implements System.IObserver(Of BaggageInfo).OnError
        ' No implementation.
    End Sub

    ' Update information.
    Public Overridable Sub OnNext(ByVal info As BaggageInfo) Implements System.IObserver(Of BaggageInfo).OnNext
        Dim updated As Boolean = False

        ' Flight has unloaded its baggage; remove from the monitor.
        If info.Carousel = 0 Then
            Dim flightsToRemove As New List(Of String)
            Dim flightNo As String = String.Format("{0,5}", info.FlightNumber)
            For Each flightInfo In flightInfos
                If flightInfo.Substring(21, 5).Equals(flightNo) Then
                    flightsToRemove.Add(flightInfo)
                    updated = True
                End If
            Next
            For Each flightToRemove In flightsToRemove
                flightInfos.Remove(flightToRemove)
            Next
            flightsToRemove.Clear()
        Else
            ' Add flight if it does not exist in the collection.
            Dim flightInfo As String = String.Format(fmt, info.From, info.FlightNumber, info.Carousel)
            If Not flightInfos.Contains(flightInfo) Then
                flightInfos.Add(flightInfo)
                updated = True
            End If
        End If
        If updated Then
            flightInfos.Sort()
            Console.WriteLine("Arrivals information from {0}", Me.name)
            For Each flightInfo In flightInfos
                Console.WriteLine(flightInfo)
            Next
            Console.WriteLine()
        End If
    End Sub
End Class

ArrivalsMonitor クラスには、SubscribeメソッドとUnsubscribeメソッドが含まれます。 Subscribe メソッドを使用すると、クラスはプライベート変数にIDisposableする呼び出しによって返されるSubscribe実装を保存できます。 Unsubscribe メソッドを使用すると、クラスはプロバイダーのDispose実装を呼び出すことによって、通知のサブスクライブを解除できます。 ArrivalsMonitor には、 OnNextOnError、および OnCompleted メソッドの実装も用意されています。 大量のコードが含まれているのは、 OnNext 実装だけです。 このメソッドは、到着便の出発地空港と手荷物が利用可能なカルーセルに関する情報を保持する、プライベートで並べ替えられた汎用の List<T> オブジェクトで動作します。 BaggageHandler クラスが新しいフライト到着を報告する場合、OnNext メソッドの実装は、そのフライトに関する情報を一覧に追加します。 BaggageHandlerクラスがフライトの手荷物がアンロードされたことを報告した場合、OnNextメソッドはそのフライトをリストから削除します。 変更が行われるたびに、リストが並べ替えられてコンソールに表示されます。

次の例には、 BaggageHandler クラスと ArrivalsMonitor クラスの 2 つのインスタンスをインスタンス化し、 BaggageHandler.BaggageStatus メソッドを使用して到着フライトに関する情報を追加および削除するアプリケーション エントリ ポイントが含まれています。 いずれの場合も、オブザーバーは更新プログラムを受け取り、手荷物受取情報を正しく表示します。

using Observables.Example;

BaggageHandler provider = new();
ArrivalsMonitor observer1 = new("BaggageClaimMonitor1");
ArrivalsMonitor observer2 = new("SecurityExit");

provider.BaggageStatus(712, "Detroit", 3);
observer1.Subscribe(provider);

provider.BaggageStatus(712, "Kalamazoo", 3);
provider.BaggageStatus(400, "New York-Kennedy", 1);
provider.BaggageStatus(712, "Detroit", 3);
observer2.Subscribe(provider);

provider.BaggageStatus(511, "San Francisco", 2);
provider.BaggageStatus(712);
observer2.Unsubscribe();

provider.BaggageStatus(400);
provider.LastBaggageClaimed();

// Sample output:
//   Arrivals information from BaggageClaimMonitor1
//   Detroit                712    3
//   
//   Arrivals information from BaggageClaimMonitor1
//   Detroit                712    3
//   Kalamazoo              712    3
//   
//   Arrivals information from BaggageClaimMonitor1
//   Detroit                712    3
//   Kalamazoo              712    3
//   New York-Kennedy       400    1
//   
//   Arrivals information from SecurityExit
//   Detroit                712    3
//   
//   Arrivals information from SecurityExit
//   Detroit                712    3
//   Kalamazoo              712    3
//   
//   Arrivals information from SecurityExit
//   Detroit                712    3
//   Kalamazoo              712    3
//   New York-Kennedy       400    1
//   
//   Arrivals information from BaggageClaimMonitor1
//   Detroit                712    3
//   Kalamazoo              712    3
//   New York-Kennedy       400    1
//   San Francisco          511    2
//   
//   Arrivals information from SecurityExit
//   Detroit                712    3
//   Kalamazoo              712    3
//   New York-Kennedy       400    1
//   San Francisco          511    2
//   
//   Arrivals information from BaggageClaimMonitor1
//   New York-Kennedy       400    1
//   San Francisco          511    2
//   
//   Arrivals information from SecurityExit
//   New York-Kennedy       400    1
//   San Francisco          511    2
//   
//   Arrivals information from BaggageClaimMonitor1
//   San Francisco          511    2
Module Example
    Public Sub Main()
        Dim provider As New BaggageHandler()
        Dim observer1 As New ArrivalsMonitor("BaggageClaimMonitor1")
        Dim observer2 As New ArrivalsMonitor("SecurityExit")

        provider.BaggageStatus(712, "Detroit", 3)
        observer1.Subscribe(provider)
        provider.BaggageStatus(712, "Kalamazoo", 3)
        provider.BaggageStatus(400, "New York-Kennedy", 1)
        provider.BaggageStatus(712, "Detroit", 3)
        observer2.Subscribe(provider)
        provider.BaggageStatus(511, "San Francisco", 2)
        provider.BaggageStatus(712)
        observer2.Unsubscribe()
        provider.BaggageStatus(400)
        provider.LastBaggageClaimed()
    End Sub
End Module
' The example displays the following output:
'      Arrivals information from BaggageClaimMonitor1
'      Detroit                712    3
'
'      Arrivals information from BaggageClaimMonitor1
'      Detroit                712    3
'      Kalamazoo              712    3
'
'      Arrivals information from BaggageClaimMonitor1
'      Detroit                712    3
'      Kalamazoo              712    3
'      New York-Kennedy       400    1
'
'      Arrivals information from SecurityExit
'      Detroit                712    3
'
'      Arrivals information from SecurityExit
'      Detroit                712    3
'      Kalamazoo              712    3
'
'      Arrivals information from SecurityExit
'      Detroit                712    3
'      Kalamazoo              712    3
'      New York-Kennedy       400    1
'
'      Arrivals information from BaggageClaimMonitor1
'      Detroit                712    3
'      Kalamazoo              712    3
'      New York-Kennedy       400    1
'      San Francisco          511    2
'
'      Arrivals information from SecurityExit
'      Detroit                712    3
'      Kalamazoo              712    3
'      New York-Kennedy       400    1
'      San Francisco          511    2
'
'      Arrivals information from BaggageClaimMonitor1
'      New York-Kennedy       400    1
'      San Francisco          511    2
'
'      Arrivals information from SecurityExit
'      New York-Kennedy       400    1
'      San Francisco          511    2
'
'      Arrivals information from BaggageClaimMonitor1
'      San Francisco          511    2
タイトル 説明
オブザーバー デザイン パターンのベスト プラクティス オブザーバー 設計パターンを実装するアプリケーションを開発するときに採用するベスト プラクティスについて説明します。
方法: プロバイダーを実装する 温度監視アプリケーションのプロバイダーの段階的な実装を提供します。
方法: オブザーバーを実装する 温度監視アプリケーションのオブザーバーの段階的な実装を提供します。