Nota
L-aċċess għal din il-paġna jeħtieġ l-awtorizzazzjoni. Tista’ tipprova tidħol jew tibdel id-direttorji.
L-aċċess għal din il-paġna jeħtieġ l-awtorizzazzjoni. Tista’ tipprova tibdel id-direttorji.
The observer design pattern enables a subscriber to register with and receive notifications from a provider. It's suitable for any scenario that requires push-based notification. The pattern defines a provider (also known as a subject or an observable) and zero, one, or more observers. Observers register with the provider, and whenever a predefined condition, event, or state change occurs, the provider automatically notifies all observers by calling a delegate. In this method call, the provider can also provide current state information to observers. In .NET, the observer design pattern is applied by implementing the generic System.IObservable<T> and System.IObserver<T> interfaces. The generic type parameter represents the type that provides notification information.
When to apply the pattern
The observer design pattern is suitable for distributed push-based notifications, because it supports a clean separation between two different components or application layers, such as a data source (business logic) layer and a user interface (display) layer. The pattern can be implemented whenever a provider uses callbacks to supply its clients with current information.
Implementing the pattern requires that you provide the following details:
A provider or subject, which is the object that sends notifications to observers. A provider is a class or structure that implements the IObservable<T> interface. The provider must implement a single method, IObservable<T>.Subscribe, which is called by observers that wish to receive notifications from the provider.
An observer, which is an object that receives notifications from a provider. An observer is a class or structure that implements the IObserver<T> interface. The observer must implement three methods, all of which are called by the provider:
- IObserver<T>.OnNext, which supplies the observer with new or current information.
- IObserver<T>.OnError, which informs the observer that an error has occurred.
- IObserver<T>.OnCompleted, which indicates that the provider has finished sending notifications.
A mechanism that allows the provider to keep track of observers. Typically, the provider uses a container object, such as a System.Collections.Generic.List<T> object, to hold references to the IObserver<T> implementations that have subscribed to notifications. Using a storage container for this purpose enables the provider to handle zero to an unlimited number of observers. The order in which observers receive notifications isn't defined; the provider is free to use any method to determine the order.
An IDisposable implementation that enables the provider to remove observers when notification is complete. Observers receive a reference to the IDisposable implementation from the Subscribe method, so they can also call the IDisposable.Dispose method to unsubscribe before the provider has finished sending notifications.
An object that contains the data that the provider sends to its observers. The type of this object corresponds to the generic type parameter of the IObservable<T> and IObserver<T> interfaces. Although this object can be the same as the IObservable<T> implementation, most commonly it's a separate type.
Note
In addition to implementing the observer design pattern, you might be interested in exploring libraries that are built using the IObservable<T> and IObserver<T> interfaces. For example, Reactive Extensions for .NET (Rx) consist of a set of extension methods and LINQ standard sequence operators to support asynchronous programming.
When to consider alternatives
The IObservable<T>/IObserver<T> interfaces are well suited for push-based notification scenarios, but .NET offers other patterns that might be a better fit:
- Standard .NET events — For simple notification scenarios within a single application, events are more idiomatic and easier to implement.
IAsyncEnumerable<T>— For async pull-based sequences where the consumer controls the pace, use async streams.System.Threading.Channels— For producer-consumer patterns with backpressure and async support, use System.Threading.Channels.- Reactive Extensions (Rx.NET) — For complex event composition, filtering, and transformation, use the
System.Reactivepackage instead of implementingIObservable<T>directly.
The most prominent use of IObservable<T> in .NET is DiagnosticListener, which lets framework and library authors emit structured diagnostic events that consumers subscribe to.
Implement the pattern
The following example uses the observer design pattern to implement an airport baggage claim information system. A BaggageInfo class provides information about arriving flights and the carousels where baggage from each flight is available for pickup. It's shown in the following example.
namespace Observables.Example;
public readonly record struct BaggageInfo(
int FlightNumber,
string From,
int Carousel);
Namespace Example
Public Structure BaggageInfo
Implements IEquatable(Of BaggageInfo)
Public ReadOnly Property FlightNumber As Integer
Public ReadOnly Property From As String
Public ReadOnly Property Carousel As Integer
Public Sub New(flightNumber As Integer, from As String, carousel As Integer)
Me.FlightNumber = flightNumber
Me.From = from
Me.Carousel = carousel
End Sub
Public Overloads Function Equals(other As BaggageInfo) As Boolean Implements IEquatable(Of BaggageInfo).Equals
Return FlightNumber = other.FlightNumber AndAlso
From = other.From AndAlso
Carousel = other.Carousel
End Function
Public Overrides Function Equals(obj As Object) As Boolean
If TypeOf obj Is BaggageInfo Then
Return Equals(DirectCast(obj, BaggageInfo))
End If
Return False
End Function
Public Overrides Function GetHashCode() As Integer
Return HashCode.Combine(FlightNumber, From, Carousel)
End Function
Public Shared Operator =(left As BaggageInfo, right As BaggageInfo) As Boolean
Return left.Equals(right)
End Operator
Public Shared Operator <>(left As BaggageInfo, right As BaggageInfo) As Boolean
Return Not left.Equals(right)
End Operator
End Structure
End Namespace
A BaggageHandler class is responsible for receiving information about arriving flights and baggage claim carousels. Internally, it maintains two collections:
_observers: A collection of clients that observe updated information._flights: A collection of flights and their assigned carousels.
The source code for the BaggageHandler class is shown in the following example.
namespace Observables.Example;
public sealed class BaggageHandler : IObservable<BaggageInfo>
{
private readonly Lock _lock = new();
private readonly HashSet<IObserver<BaggageInfo>> _observers = [];
private readonly HashSet<BaggageInfo> _flights = [];
public IDisposable Subscribe(IObserver<BaggageInfo> observer)
{
BaggageInfo[] snapshot;
lock (_lock)
{
// Check whether observer is already registered. If not, add it.
if (!_observers.Add(observer))
{
return new Unsubscriber<BaggageInfo>(_lock, _observers, observer);
}
// Snapshot existing data while holding the lock.
snapshot = [.. _flights];
}
// Provide observer with existing data outside the lock.
foreach (BaggageInfo item in snapshot)
{
observer.OnNext(item);
}
return new Unsubscriber<BaggageInfo>(_lock, _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);
IObserver<BaggageInfo>[] snapshot;
// Carousel is assigned, so add new info object to list.
if (carousel > 0)
{
lock (_lock)
{
if (!_flights.Add(info))
{
return;
}
snapshot = [.. _observers];
}
foreach (IObserver<BaggageInfo> observer in snapshot)
{
observer.OnNext(info);
}
}
else if (carousel is 0)
{
// Baggage claim for flight is done.
lock (_lock)
{
if (_flights.RemoveWhere(
flight => flight.FlightNumber == info.FlightNumber) == 0)
{
return;
}
snapshot = [.. _observers];
}
foreach (IObserver<BaggageInfo> observer in snapshot)
{
observer.OnNext(info);
}
}
}
public void LastBaggageClaimed()
{
IObserver<BaggageInfo>[] snapshot;
lock (_lock)
{
snapshot = [.. _observers];
_observers.Clear();
}
foreach (IObserver<BaggageInfo> observer in snapshot)
{
observer.OnCompleted();
}
}
}
Namespace Example
Public NotInheritable Class BaggageHandler
Implements IObservable(Of BaggageInfo)
Private ReadOnly _lock As New Object()
Private ReadOnly _observers As New HashSet(Of IObserver(Of BaggageInfo))()
Private ReadOnly _flights As New HashSet(Of BaggageInfo)()
Public Function Subscribe(observer As IObserver(Of BaggageInfo)) As IDisposable Implements IObservable(Of BaggageInfo).Subscribe
Dim snapshot As BaggageInfo()
SyncLock _lock
' Check whether observer is already registered. If not, add it.
If Not _observers.Add(observer) Then
Return New Unsubscriber(Of BaggageInfo)(_lock, _observers, observer)
End If
' Snapshot existing data while holding the lock.
snapshot = _flights.ToArray()
End SyncLock
' Provide observer with existing data outside the lock.
For Each item As BaggageInfo In snapshot
observer.OnNext(item)
Next
Return New Unsubscriber(Of BaggageInfo)(_lock, _observers, observer)
End Function
' Called to indicate all baggage is now unloaded.
Public Sub BaggageStatus(flightNumber As Integer)
BaggageStatus(flightNumber, String.Empty, 0)
End Sub
Public Sub BaggageStatus(flightNumber As Integer, from As String, carousel As Integer)
Dim info As New BaggageInfo(flightNumber, from, carousel)
Dim snapshot As IObserver(Of BaggageInfo)()
' Carousel is assigned, so add new info object to list.
If carousel > 0 Then
SyncLock _lock
If Not _flights.Add(info) Then
Return
End If
snapshot = _observers.ToArray()
End SyncLock
For Each observer As IObserver(Of BaggageInfo) In snapshot
observer.OnNext(info)
Next
ElseIf carousel = 0 Then
' Baggage claim for flight is done.
SyncLock _lock
If _flights.RemoveWhere(
Function(flight) flight.FlightNumber = info.FlightNumber) = 0 Then
Return
End If
snapshot = _observers.ToArray()
End SyncLock
For Each observer As IObserver(Of BaggageInfo) In snapshot
observer.OnNext(info)
Next
End If
End Sub
Public Sub LastBaggageClaimed()
Dim snapshot As IObserver(Of BaggageInfo)()
SyncLock _lock
snapshot = _observers.ToArray()
_observers.Clear()
End SyncLock
For Each observer As IObserver(Of BaggageInfo) In snapshot
observer.OnCompleted()
Next
End Sub
End Class
End Namespace
Clients that wish to receive updated information call the BaggageHandler.Subscribe method. If the client hasn't previously subscribed to notifications, a reference to the client's IObserver<T> implementation is added to the _observers collection.
The overloaded BaggageHandler.BaggageStatus method can be called to indicate that baggage from a flight either is being unloaded or is no longer being unloaded. In the first case, the method is passed a flight number, the airport from which the flight originated, and the carousel where baggage is being unloaded. In the second case, the method is passed only a flight number. For baggage that is being unloaded, the method checks whether the BaggageInfo information passed to the method exists in the _flights collection. If it doesn't, the method adds the information and calls each observer's OnNext method. For flights whose baggage is no longer being unloaded, the method checks whether information on that flight is stored in the _flights collection. If it is, the method calls each observer's OnNext method and removes the BaggageInfo object from the _flights collection.
When the last flight of the day has landed and its baggage has been processed, the BaggageHandler.LastBaggageClaimed method is called. This method calls each observer's OnCompleted method to indicate that all notifications have completed, and then clears the _observers collection.
The provider's Subscribe method returns an IDisposable implementation that enables observers to stop receiving notifications before the OnCompleted method is called. The source code for this Unsubscriber class is shown in the following example. When the class is instantiated in the BaggageHandler.Subscribe method, it's passed a reference to the _lock object, the _observers collection, and a reference to the observer that is added to the collection. These references are assigned to local variables. When the object's Dispose method is called, it removes the observer from the _observers collection within a lock.
namespace Observables.Example;
internal sealed class Unsubscriber<T> : IDisposable
{
private readonly Lock _lock;
private readonly ISet<IObserver<T>> _observers;
private readonly IObserver<T> _observer;
internal Unsubscriber(
Lock @lock,
ISet<IObserver<T>> observers,
IObserver<T> observer) => (_lock, _observers, _observer) = (@lock, observers, observer);
public void Dispose()
{
lock (_lock)
{
_observers.Remove(_observer);
}
}
}
Namespace Example
Friend NotInheritable Class Unsubscriber(Of T)
Implements IDisposable
Private ReadOnly _lock As Object
Private ReadOnly _observers As ISet(Of IObserver(Of T))
Private ReadOnly _observer As IObserver(Of T)
Friend Sub New(lock As Object, observers As ISet(Of IObserver(Of T)), observer As IObserver(Of T))
_lock = lock
_observers = observers
_observer = observer
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
SyncLock _lock
_observers.Remove(_observer)
End SyncLock
End Sub
End Class
End Namespace
The following example provides an IObserver<T> implementation named ArrivalsMonitor, which is a base class that displays baggage claim information. The information is displayed alphabetically, by the name of the originating city. The methods of ArrivalsMonitor are marked as overridable (in Visual Basic) or virtual (in C#), so they can be overridden in a derived class.
namespace Observables.Example;
public class ArrivalsMonitor : IObserver<BaggageInfo>
{
private readonly string _name;
private readonly Lock _lock = new();
private readonly List<string> _flights = [];
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()
{
Interlocked.Exchange(ref _cancellation, null)?.Dispose();
lock (_lock)
{
_flights.Clear();
}
}
public virtual void OnCompleted()
{
lock (_lock)
{
_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;
lock (_lock)
{
// Flight has unloaded its baggage; remove from the monitor.
if (info.Carousel is 0)
{
string flightNumber = $"{info.FlightNumber,5}";
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();
}
}
}
}
Imports System.Threading
Namespace Example
Public Class ArrivalsMonitor
Implements IObserver(Of BaggageInfo)
Private ReadOnly _name As String
Private ReadOnly _lock As New Object()
Private ReadOnly _flights As New List(Of String)()
Private ReadOnly _format As String = "{0,-20} {1,5} {2, 3}"
Private _cancellation As IDisposable
Public Sub New(name As String)
If String.IsNullOrEmpty(name) Then
Throw New ArgumentException("Value cannot be null or empty.", NameOf(name))
End If
_name = name
End Sub
Public Overridable Sub Subscribe(provider As BaggageHandler)
_cancellation = provider.Subscribe(Me)
End Sub
Public Overridable Sub Unsubscribe()
Dim previous = Interlocked.Exchange(_cancellation, Nothing)
previous?.Dispose()
SyncLock _lock
_flights.Clear()
End SyncLock
End Sub
Public Overridable Sub OnCompleted() Implements IObserver(Of BaggageInfo).OnCompleted
SyncLock _lock
_flights.Clear()
End SyncLock
End Sub
' No implementation needed: Method is not called by the BaggageHandler class.
Public Overridable Sub OnError([error] As Exception) Implements IObserver(Of BaggageInfo).OnError
' No implementation.
End Sub
' Update information.
Public Overridable Sub OnNext(info As BaggageInfo) Implements IObserver(Of BaggageInfo).OnNext
Dim updated As Boolean = False
SyncLock _lock
' Flight has unloaded its baggage; remove from the monitor.
If info.Carousel = 0 Then
Dim flightNumber As String = String.Format("{0,5}", info.FlightNumber)
For index As Integer = _flights.Count - 1 To 0 Step -1
Dim flightInfo As String = _flights(index)
If flightInfo.Substring(21, 5).Equals(flightNumber) Then
updated = True
_flights.RemoveAt(index)
End If
Next
Else
' Add flight if it doesn't exist in the collection.
Dim flightInfo As String = String.Format(_format, info.From, info.FlightNumber, info.Carousel)
If Not _flights.Contains(flightInfo) Then
_flights.Add(flightInfo)
updated = True
End If
End If
If updated Then
_flights.Sort()
Console.WriteLine($"Arrivals information from {_name}")
For Each flightInfo As String In _flights
Console.WriteLine(flightInfo)
Next
Console.WriteLine()
End If
End SyncLock
End Sub
End Class
End Namespace
The ArrivalsMonitor class includes the Subscribe and Unsubscribe methods. The Subscribe method enables the class to save the IDisposable implementation returned by the call to Subscribe to a private variable. The Unsubscribe method enables the class to unsubscribe from notifications by calling the provider's Dispose implementation. ArrivalsMonitor also provides implementations of the OnNext, OnError, and OnCompleted methods. Only the OnNext implementation contains a significant amount of code. The method works with a private, sorted, generic List<T> object that maintains information about the airports of origin for arriving flights and the carousels on which their baggage is available. If the BaggageHandler class reports a new flight arrival, the OnNext method implementation adds information about that flight to the list. If the BaggageHandler class reports that the flight's baggage has been unloaded, the OnNext method removes that flight from the list. Whenever a change is made, the list is sorted and displayed to the console.
The following example contains the application entry point that instantiates the BaggageHandler class and two instances of the ArrivalsMonitor class, and uses the BaggageHandler.BaggageStatus method to add and remove information about arriving flights. In each case, the observers receive updates and correctly display baggage claim information.
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();
Imports Observables.Example
Imports System.Threading
Module Program
Sub Main(args As String())
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