Практическое руководство. Реализация поставщика

Шаблон разработки наблюдателя подразумевает разделение ролей поставщика, который отслеживает данные и отправляет уведомления, и одного или нескольких наблюдателей, которые получают от поставщика уведомления (обратные вызовы). В этой статье описан процесс создания поставщика. Создание наблюдателя рассматривается в схожей статье Практическое руководство. Реализация объекта Observer.

Создание поставщика

  1. Определите данные, за отправку которых наблюдателям будет отвечать этот поставщик. Сам поставщик и отправляемые им данные могут иметь одинаковый тип, но обычно они представлены разными типами. Например, в приложении контроля температуры структура Temperature определяет данные, которые отслеживаются поставщиком (он представлен классом TemperatureMonitor, который мы определим на следующем этапе) и на которые подписываются наблюдатели.

    using System;
    
    public struct Temperature
    {
       private decimal temp;
       private DateTime tempDate;
    
       public Temperature(decimal temperature, DateTime dateAndTime)
       {
          this.temp = temperature;
          this.tempDate = dateAndTime;
       }
    
       public decimal Degrees
       { get { return this.temp; } }
    
       public DateTime Date
       { get { return this.tempDate; } }
    }
    
    Public Structure Temperature
        Private temp As Decimal
        Private tempDate As DateTime
    
        Public Sub New(ByVal temperature As Decimal, ByVal dateAndTime As DateTime)
            Me.temp = temperature
            Me.tempDate = dateAndTime
        End Sub
    
        Public ReadOnly Property Degrees As Decimal
            Get
                Return Me.temp
            End Get
        End Property
    
        Public ReadOnly Property [Date] As DateTime
            Get
                Return tempDate
            End Get
        End Property
    End Structure
    
  2. Определите поставщик данных в виде типа, реализующего интерфейс System.IObservable<T>. Аргумент универсального типа для поставщика определяет тип данных, отправляемых наблюдателям. В следующем примере определяется класс TemperatureMonitor, который представляет собой реализацию System.IObservable<T> с аргументом универсального типа Temperature.

    using System;
    using System.Collections.Generic;
    
    public class TemperatureMonitor : IObservable<Temperature>
    {
    
    Imports System.Collections.Generic
    
    
    Public Class TemperatureMonitor : Implements IObservable(Of Temperature)
    
  3. Выберите способ, который поставщик будет использовать для хранения ссылок на наблюдатели, которые он должен извещать при соответствующих условиях. Чаще всего для этого используется объект коллекции, например универсальный объект List<T>. В следующем примере определяется закрытый объект List<T>, экземпляр которого создается в конструкторе класса TemperatureMonitor.

    using System;
    using System.Collections.Generic;
    
    public class TemperatureMonitor : IObservable<Temperature>
    {
       List<IObserver<Temperature>> observers;
    
       public TemperatureMonitor()
       {
          observers = new List<IObserver<Temperature>>();
       }
    
    Imports System.Collections.Generic
    
    
    Public Class TemperatureMonitor : Implements IObservable(Of Temperature)
        Dim observers As List(Of IObserver(Of Temperature))
    
        Public Sub New()
            observers = New List(Of IObserver(Of Temperature))
        End Sub
    
  4. Определите реализацию IDisposable, которую поставщик передает подписчикам, чтобы они могли в любой момент отключить отправку уведомлений. В следующем примере определяется вложенный класс Unsubscriber, в который передается ссылка на коллекцию подписчиков и на отдельного подписчика при создании экземпляра класса. Этот код позволяет подписчику вызвать для объекта реализацию IDisposable.Dispose, чтобы удалить себя из коллекции подписчиков.

    private class Unsubscriber : IDisposable
    {
       private List<IObserver<Temperature>> _observers;
       private IObserver<Temperature> _observer;
    
       public Unsubscriber(List<IObserver<Temperature>> observers, IObserver<Temperature> observer)
       {
          this._observers = observers;
          this._observer = observer;
       }
    
       public void Dispose()
       {
          if (! (_observer == null)) _observers.Remove(_observer);
       }
    }
    
    Private Class Unsubscriber : Implements IDisposable
        Private _observers As List(Of IObserver(Of Temperature))
        Private _observer As IObserver(Of Temperature)
    
        Public Sub New(ByVal observers As List(Of IObserver(Of Temperature)), ByVal observer As IObserver(Of Temperature))
            Me._observers = observers
            Me._observer = observer
        End Sub
    
        Public Sub Dispose() Implements IDisposable.Dispose
            If _observer IsNot Nothing Then _observers.Remove(_observer)
        End Sub
    End Class
    
  5. Реализуйте метод IObservable<T>.Subscribe. В этот метод передается ссылка на интерфейс System.IObserver<T>, которую нужно сохранить в объекте, созданном для этой цели на шаге 3. Затем метод должен возвращать реализацию IDisposable, разработанную на шаге 4. Ниже представлен пример реализации метода Subscribe в классе TemperatureMonitor.

    public IDisposable Subscribe(IObserver<Temperature> observer)
    {
       if (! observers.Contains(observer))
          observers.Add(observer);
    
       return new Unsubscriber(observers, observer);
    }
    
    Public Function Subscribe(ByVal observer As System.IObserver(Of Temperature)) As System.IDisposable Implements System.IObservable(Of Temperature).Subscribe
        If Not observers.Contains(observer) Then
            observers.Add(observer)
        End If
        Return New Unsubscriber(observers, observer)
    End Function
    
  6. Организуйте уведомление наблюдателей, вызывая их реализации методов IObserver<T>.OnNext, IObserver<T>.OnError и IObserver<T>.OnCompleted. В некоторых случаях поставщик может не вызывать метод OnError при возникновении ошибки. Например, при помощи указанного ниже метода GetTemperature моделируется монитор, который считывает температурные данные каждые пять секунд и уведомляет наблюдателей, если температура изменилась по крайней мере на 0,1 градуса с момента предыдущего измерения. Если устройство не передает данные о температуре (то есть получено значение null), поставщик уведомляет наблюдатели о завершении передачи. Обратите внимание, что помимо вызова метода OnCompleted для каждого наблюдателя, метод GetTemperature инициирует очистку коллекции List<T>. В этом случае поставщик не вызывает метод OnError для своих наблюдателей.

    public void GetTemperature()
    {
       // Create an array of sample data to mimic a temperature device.
       Nullable<Decimal>[] temps = {14.6m, 14.65m, 14.7m, 14.9m, 14.9m, 15.2m, 15.25m, 15.2m,
                                    15.4m, 15.45m, null };
       // Store the previous temperature, so notification is only sent after at least .1 change.
       Nullable<Decimal> previous = null;
       bool start = true;
    
       foreach (var temp in temps) {
          System.Threading.Thread.Sleep(2500);
          if (temp.HasValue) {
             if (start || (Math.Abs(temp.Value - previous.Value) >= 0.1m )) {
                Temperature tempData = new Temperature(temp.Value, DateTime.Now);
                foreach (var observer in observers)
                   observer.OnNext(tempData);
                previous = temp;
                if (start) start = false;
             }
          }
          else {
             foreach (var observer in observers.ToArray())
                if (observer != null) observer.OnCompleted();
    
             observers.Clear();
             break;
          }
       }
    }
    
    Public Sub GetTemperature()
        ' Create an array of sample data to mimic a temperature device.
        Dim temps() As Nullable(Of Decimal) = {14.6D, 14.65D, 14.7D, 14.9D, 14.9D, 15.2D, 15.25D, 15.2D,
                                              15.4D, 15.45D, Nothing}
        ' Store the previous temperature, so notification is only sent after at least .1 change.
        Dim previous As Nullable(Of Decimal)
        Dim start As Boolean = True
    
        For Each temp In temps
            System.Threading.Thread.Sleep(2500)
    
            If temp.HasValue Then
                If start OrElse Math.Abs(temp.Value - previous.Value) >= 0.1 Then
                    Dim tempData As New Temperature(temp.Value, Date.Now)
                    For Each observer In observers
                        observer.OnNext(tempData)
                    Next
                    previous = temp
                    If start Then start = False
                End If
            Else
                For Each observer In observers.ToArray()
                    If observer IsNot Nothing Then observer.OnCompleted()
                Next
                observers.Clear()
                Exit For
            End If
        Next
    End Sub
    

Пример

Ниже представлен полный пример исходного кода, определяющий реализацию IObservable<T> для приложения контроля температуры. Он содержит структуру Temperature для данных, отправляемых наблюдателям, и класс TemperatureMonitor, который является реализацией IObservable<T>.

using System.Threading;
using System;
using System.Collections.Generic;

public class TemperatureMonitor : IObservable<Temperature>
{
   List<IObserver<Temperature>> observers;

   public TemperatureMonitor()
   {
      observers = new List<IObserver<Temperature>>();
   }

   private class Unsubscriber : IDisposable
   {
      private List<IObserver<Temperature>> _observers;
      private IObserver<Temperature> _observer;

      public Unsubscriber(List<IObserver<Temperature>> observers, IObserver<Temperature> observer)
      {
         this._observers = observers;
         this._observer = observer;
      }

      public void Dispose()
      {
         if (! (_observer == null)) _observers.Remove(_observer);
      }
   }

   public IDisposable Subscribe(IObserver<Temperature> observer)
   {
      if (! observers.Contains(observer))
         observers.Add(observer);

      return new Unsubscriber(observers, observer);
   }

   public void GetTemperature()
   {
      // Create an array of sample data to mimic a temperature device.
      Nullable<Decimal>[] temps = {14.6m, 14.65m, 14.7m, 14.9m, 14.9m, 15.2m, 15.25m, 15.2m,
                                   15.4m, 15.45m, null };
      // Store the previous temperature, so notification is only sent after at least .1 change.
      Nullable<Decimal> previous = null;
      bool start = true;

      foreach (var temp in temps) {
         System.Threading.Thread.Sleep(2500);
         if (temp.HasValue) {
            if (start || (Math.Abs(temp.Value - previous.Value) >= 0.1m )) {
               Temperature tempData = new Temperature(temp.Value, DateTime.Now);
               foreach (var observer in observers)
                  observer.OnNext(tempData);
               previous = temp;
               if (start) start = false;
            }
         }
         else {
            foreach (var observer in observers.ToArray())
               if (observer != null) observer.OnCompleted();

            observers.Clear();
            break;
         }
      }
   }
}
Imports System.Threading
Imports System.Collections.Generic


Public Class TemperatureMonitor : Implements IObservable(Of Temperature)
    Dim observers As List(Of IObserver(Of Temperature))

    Public Sub New()
        observers = New List(Of IObserver(Of Temperature))
    End Sub

    Private Class Unsubscriber : Implements IDisposable
        Private _observers As List(Of IObserver(Of Temperature))
        Private _observer As IObserver(Of Temperature)

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

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

    Public Function Subscribe(ByVal observer As System.IObserver(Of Temperature)) As System.IDisposable Implements System.IObservable(Of Temperature).Subscribe
        If Not observers.Contains(observer) Then
            observers.Add(observer)
        End If
        Return New Unsubscriber(observers, observer)
    End Function

    Public Sub GetTemperature()
        ' Create an array of sample data to mimic a temperature device.
        Dim temps() As Nullable(Of Decimal) = {14.6D, 14.65D, 14.7D, 14.9D, 14.9D, 15.2D, 15.25D, 15.2D,
                                              15.4D, 15.45D, Nothing}
        ' Store the previous temperature, so notification is only sent after at least .1 change.
        Dim previous As Nullable(Of Decimal)
        Dim start As Boolean = True

        For Each temp In temps
            System.Threading.Thread.Sleep(2500)

            If temp.HasValue Then
                If start OrElse Math.Abs(temp.Value - previous.Value) >= 0.1 Then
                    Dim tempData As New Temperature(temp.Value, Date.Now)
                    For Each observer In observers
                        observer.OnNext(tempData)
                    Next
                    previous = temp
                    If start Then start = False
                End If
            Else
                For Each observer In observers.ToArray()
                    If observer IsNot Nothing Then observer.OnCompleted()
                Next
                observers.Clear()
                Exit For
            End If
        Next
    End Sub
End Class

См. также