Implementación de un proveedor

El patrón de diseño del observador requiere una división entre un proveedor, que supervisa los datos y envía notificaciones, y uno o varios observadores, que reciben notificaciones (devoluciones de llamada) del proveedor. En este artículo se muestra cómo crear un proveedor. Para obtener información sobre cómo crear un observador, consulte Implementación de un observador.

Definición del tipo de datos

Defina los datos que el proveedor envía a los observadores. Aunque el proveedor y los datos que envía a los observadores pueden ser de un solo tipo, normalmente un tipo diferente representa cada uno. Por ejemplo, en una aplicación de supervisión de temperatura, la Temperature estructura define los datos que supervisa la TemperatureMonitor clase (definida en la sección siguiente) y a qué observadores se suscriben.

namespace TemperatureSample;

public readonly record struct Temperature(decimal Degrees, DateTime Date);
Namespace Global.TemperatureSample

    Public Structure Temperature
        Public ReadOnly Property Degrees As Decimal
        Public ReadOnly Property [Date] As Date

        Public Sub New(degrees As Decimal, [date] As Date)
            Me.Degrees = degrees
            Me.Date = [date]
        End Sub
    End Structure

End Namespace

Creación de un proveedor

El proveedor de datos es un tipo que implementa la System.IObservable<T> interfaz . El argumento de tipo genérico del proveedor es el tipo que envía a los observadores.

  1. Defina la clase de proveedor. En el ejemplo siguiente se define una TemperatureMonitor clase , que es una implementación construida System.IObservable<T> con un argumento de tipo genérico de Temperature.

    namespace TemperatureSample;
    
    public sealed class TemperatureMonitor : IObservable<Temperature>
    {
    
    Imports System.Threading
    Imports System.Threading.Tasks
    
    Namespace Global.TemperatureSample
    
        Public NotInheritable Class TemperatureMonitor
            Implements IObservable(Of Temperature)
    
  2. Agregue un campo para almacenar referencias de observador.

    El proveedor debe realizar un seguimiento de cada observador registrado para que pueda enviar notificaciones más adelante. Normalmente, use un objeto de colección como un objeto genérico List<T> . En el ejemplo siguiente se define un objeto privado List<T> al que se crea una instancia en el constructor de clase TemperatureMonitor .

    namespace TemperatureSample;
    
    public sealed class TemperatureMonitor : IObservable<Temperature>
    {
        private readonly List<IObserver<Temperature>> _observers = [];
        private readonly Lock _sync = new();
    
    Imports System.Threading
    Imports System.Threading.Tasks
    
    Namespace Global.TemperatureSample
    
        Public NotInheritable Class TemperatureMonitor
            Implements IObservable(Of Temperature)
    
            Private ReadOnly _observers As New List(Of IObserver(Of Temperature))()
            Private ReadOnly _sync As New Object()
    
  3. Defina una IDisposable implementación para anular la suscripción.

    El proveedor devuelve esta implementación a los suscriptores para que puedan dejar de recibir notificaciones en cualquier momento. En el ejemplo siguiente se define una clase anidada Unsubscriber que recibe una referencia a la colección de suscriptores y otra al suscriptor al instanciarse. La clase Unsubscriber permite que el suscriptor llame a la implementación de IDisposable.Dispose del objeto para eliminarse de la colección de suscriptores.

    private sealed class Unsubscriber(
        List<IObserver<Temperature>> observers,
        IObserver<Temperature> observer,
        Lock sync) : IDisposable
    {
        public void Dispose()
        {
            lock (sync)
            {
                observers.Remove(observer);
            }
        }
    }
    
    Private NotInheritable Class Unsubscriber
        Implements IDisposable
    
        Private ReadOnly _observers As List(Of IObserver(Of Temperature))
        Private ReadOnly _observer As IObserver(Of Temperature)
        Private ReadOnly _sync As Object
    
        Public Sub New(observers As List(Of IObserver(Of Temperature)),
                       observer As IObserver(Of Temperature),
                       sync As Object)
            _observers = observers
            _observer = observer
            _sync = sync
        End Sub
    
        Public Sub Dispose() Implements IDisposable.Dispose
            SyncLock _sync
                _observers.Remove(_observer)
            End SyncLock
        End Sub
    End Class
    
  4. Implemente el método IObservable<T>.Subscribe.

    El método recibe una referencia a la System.IObserver<T> interfaz . Guarde esa referencia en la colección de observadores del paso anterior y, a continuación, devuelva la implementación de cancelación de suscripción IDisposable. En el ejemplo siguiente se muestra la Subscribe implementación en la TemperatureMonitor clase .

    public IDisposable Subscribe(IObserver<Temperature> observer)
    {
        ArgumentNullException.ThrowIfNull(observer);
    
        lock (_sync)
        {
            if (!_observers.Contains(observer))
                _observers.Add(observer);
        }
    
        return new Unsubscriber(_observers, observer, _sync);
    }
    
    Public Function Subscribe(observer As IObserver(Of Temperature)) As IDisposable _
        Implements IObservable(Of Temperature).Subscribe
    
        ArgumentNullException.ThrowIfNull(observer)
    
        SyncLock _sync
            If Not _observers.Contains(observer) Then
                _observers.Add(observer)
            End If
        End SyncLock
    
        Return New Unsubscriber(_observers, observer, _sync)
    End Function
    
  5. Implemente la lógica de notificación llamando a los métodos IObserver<T>.OnNext, IObserver<T>.OnError y IObserver<T>.OnCompleted de los observadores.

    En algunos casos, es posible que un proveedor no llame a OnError cuando se produzca un error. El siguiente GetTemperature método simula un monitor que lee los datos de temperatura cada cinco segundos y notifica a los observadores si la temperatura ha cambiado por lo menos .1 grados desde la lectura anterior. Si el dispositivo no informa de una temperatura (es decir, si su valor es null), el proveedor notifica a los observadores que la transmisión ha finalizado llamando al método OnCompleted de cada observador y borra la colección List<T>. En este ejemplo, el proveedor nunca llama a OnError.

    public async Task GetTemperatureAsync(CancellationToken cancellationToken = default)
    {
        // Sample data that mimics a temperature device. A null value signals the end of transmission.
        decimal?[] temps =
        [
            14.6m, 14.65m, 14.7m, 14.9m, 14.9m, 15.2m,
            15.25m, 15.2m, 15.4m, 15.45m, null
        ];
    
        decimal? previous = null;
    
        foreach (decimal? temp in temps)
        {
            await Task.Delay(TimeSpan.FromSeconds(2.5), cancellationToken);
    
            if (temp is decimal value)
            {
                // Notify only after at least a 0.1° change.
                if (previous is null || Math.Abs(value - previous.Value) >= 0.1m)
                {
                    NotifyAll(new Temperature(value, DateTime.Now));
                    previous = value;
                }
            }
            else
            {
                CompleteAll();
                break;
            }
        }
    }
    
    private void NotifyAll(Temperature data)
    {
        IObserver<Temperature>[] snapshot;
        lock (_sync)
        {
            snapshot = [.. _observers];
        }
    
        foreach (IObserver<Temperature> observer in snapshot)
            observer.OnNext(data);
    }
    
    private void CompleteAll()
    {
        IObserver<Temperature>[] snapshot;
        lock (_sync)
        {
            snapshot = [.. _observers];
            _observers.Clear();
        }
    
        foreach (IObserver<Temperature> observer in snapshot)
            observer.OnCompleted();
    }
    
    Public Async Function GetTemperatureAsync(Optional cancellationToken As CancellationToken = Nothing) As Task
        ' Sample data that mimics a temperature device. A Nothing value signals the end of transmission.
        Dim temps As Decimal?() = {
            14.6D, 14.65D, 14.7D, 14.9D, 14.9D, 15.2D,
            15.25D, 15.2D, 15.4D, 15.45D, Nothing
        }
    
        Dim previous As Decimal? = Nothing
    
        For Each temp As Decimal? In temps
            Await Task.Delay(TimeSpan.FromSeconds(2.5), cancellationToken)
    
            If temp.HasValue Then
                ' Notify only after at least a 0.1° change.
                If Not previous.HasValue OrElse Math.Abs(temp.Value - previous.Value) >= 0.1D Then
                    NotifyAll(New Temperature(temp.Value, Date.Now))
                    previous = temp
                End If
            Else
                CompleteAll()
                Exit For
            End If
        Next
    End Function
    
    Private Sub NotifyAll(data As Temperature)
        Dim snapshot As IObserver(Of Temperature)()
        SyncLock _sync
            snapshot = _observers.ToArray()
        End SyncLock
    
        For Each observer In snapshot
            observer.OnNext(data)
        Next
    End Sub
    
    Private Sub CompleteAll()
        Dim snapshot As IObserver(Of Temperature)()
        SyncLock _sync
            snapshot = _observers.ToArray()
            _observers.Clear()
        End SyncLock
    
        For Each observer In snapshot
            observer.OnCompleted()
        Next
    End Sub
    

Example

El ejemplo siguiente contiene el código fuente completo de una IObservable<T> implementación para una aplicación de supervisión de temperatura. Incluye la Temperature estructura , que es los datos que el proveedor envía a los observadores y la TemperatureMonitor clase , que es la IObservable<T> implementación.

namespace TemperatureSample;

public sealed class TemperatureMonitor : IObservable<Temperature>
{
    private readonly List<IObserver<Temperature>> _observers = [];
    private readonly Lock _sync = new();

    private sealed class Unsubscriber(
        List<IObserver<Temperature>> observers,
        IObserver<Temperature> observer,
        Lock sync) : IDisposable
    {
        public void Dispose()
        {
            lock (sync)
            {
                observers.Remove(observer);
            }
        }
    }

    public IDisposable Subscribe(IObserver<Temperature> observer)
    {
        ArgumentNullException.ThrowIfNull(observer);

        lock (_sync)
        {
            if (!_observers.Contains(observer))
                _observers.Add(observer);
        }

        return new Unsubscriber(_observers, observer, _sync);
    }

    public async Task GetTemperatureAsync(CancellationToken cancellationToken = default)
    {
        // Sample data that mimics a temperature device. A null value signals the end of transmission.
        decimal?[] temps =
        [
            14.6m, 14.65m, 14.7m, 14.9m, 14.9m, 15.2m,
            15.25m, 15.2m, 15.4m, 15.45m, null
        ];

        decimal? previous = null;

        foreach (decimal? temp in temps)
        {
            await Task.Delay(TimeSpan.FromSeconds(2.5), cancellationToken);

            if (temp is decimal value)
            {
                // Notify only after at least a 0.1° change.
                if (previous is null || Math.Abs(value - previous.Value) >= 0.1m)
                {
                    NotifyAll(new Temperature(value, DateTime.Now));
                    previous = value;
                }
            }
            else
            {
                CompleteAll();
                break;
            }
        }
    }

    private void NotifyAll(Temperature data)
    {
        IObserver<Temperature>[] snapshot;
        lock (_sync)
        {
            snapshot = [.. _observers];
        }

        foreach (IObserver<Temperature> observer in snapshot)
            observer.OnNext(data);
    }

    private void CompleteAll()
    {
        IObserver<Temperature>[] snapshot;
        lock (_sync)
        {
            snapshot = [.. _observers];
            _observers.Clear();
        }

        foreach (IObserver<Temperature> observer in snapshot)
            observer.OnCompleted();
    }
}
Imports System.Threading
Imports System.Threading.Tasks

Namespace Global.TemperatureSample

    Public NotInheritable Class TemperatureMonitor
        Implements IObservable(Of Temperature)

        Private ReadOnly _observers As New List(Of IObserver(Of Temperature))()
        Private ReadOnly _sync As New Object()

        Private NotInheritable Class Unsubscriber
            Implements IDisposable

            Private ReadOnly _observers As List(Of IObserver(Of Temperature))
            Private ReadOnly _observer As IObserver(Of Temperature)
            Private ReadOnly _sync As Object

            Public Sub New(observers As List(Of IObserver(Of Temperature)),
                           observer As IObserver(Of Temperature),
                           sync As Object)
                _observers = observers
                _observer = observer
                _sync = sync
            End Sub

            Public Sub Dispose() Implements IDisposable.Dispose
                SyncLock _sync
                    _observers.Remove(_observer)
                End SyncLock
            End Sub
        End Class

        Public Function Subscribe(observer As IObserver(Of Temperature)) As IDisposable _
            Implements IObservable(Of Temperature).Subscribe

            ArgumentNullException.ThrowIfNull(observer)

            SyncLock _sync
                If Not _observers.Contains(observer) Then
                    _observers.Add(observer)
                End If
            End SyncLock

            Return New Unsubscriber(_observers, observer, _sync)
        End Function

        Public Async Function GetTemperatureAsync(Optional cancellationToken As CancellationToken = Nothing) As Task
            ' Sample data that mimics a temperature device. A Nothing value signals the end of transmission.
            Dim temps As Decimal?() = {
                14.6D, 14.65D, 14.7D, 14.9D, 14.9D, 15.2D,
                15.25D, 15.2D, 15.4D, 15.45D, Nothing
            }

            Dim previous As Decimal? = Nothing

            For Each temp As Decimal? In temps
                Await Task.Delay(TimeSpan.FromSeconds(2.5), cancellationToken)

                If temp.HasValue Then
                    ' Notify only after at least a 0.1° change.
                    If Not previous.HasValue OrElse Math.Abs(temp.Value - previous.Value) >= 0.1D Then
                        NotifyAll(New Temperature(temp.Value, Date.Now))
                        previous = temp
                    End If
                Else
                    CompleteAll()
                    Exit For
                End If
            Next
        End Function

        Private Sub NotifyAll(data As Temperature)
            Dim snapshot As IObserver(Of Temperature)()
            SyncLock _sync
                snapshot = _observers.ToArray()
            End SyncLock

            For Each observer In snapshot
                observer.OnNext(data)
            Next
        End Sub

        Private Sub CompleteAll()
            Dim snapshot As IObserver(Of Temperature)()
            SyncLock _sync
                snapshot = _observers.ToArray()
                _observers.Clear()
            End SyncLock

            For Each observer In snapshot
                observer.OnCompleted()
            Next
        End Sub
    End Class

End Namespace