Observer design pattern - .NET (original) (raw)
This browser is no longer supported.
Upgrade to Microsoft Edge to take advantage of the latest features, security updates, and technical support.
Observer design pattern
- Article
- 05/25/2023
In this article
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 invoking 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 and System.IObserver 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 interface. The provider must implement a single method, IObservable.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 interface. The observer must implement three methods, all of which are called by the provider:
- IObserver.OnNext, which supplies the observer with new or current information.
- IObserver.OnError, which informs the observer that an error has occurred.
- IObserver.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 object, to hold references to the IObserver 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 and IObserver interfaces. Although this object can be the same as the IObservable implementation, most commonly it's a separate type.
Note
In addition to implementing the observer design pattern, you may be interested in exploring libraries that are built using the IObservable and IObserver interfaces. For example, Reactive Extensions for .NET (Rx) consist of a set of extension methods and LINQ standard sequence operators to support asynchronous programming.
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);
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
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 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
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 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(Of BaggageInfo)
class is shown in the following example. When the class is instantiated in the BaggageHandler.Subscribe
method, it's passed a reference to 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 checks whether the observer still exists in the _observers
collection, and, if it does, removes the observer.
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
The following example provides an IObserver 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 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
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 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();
// 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
Related articles
Title | Description |
---|---|
Observer Design Pattern Best Practices | Describes best practices to adopt when developing applications that implement the observer design pattern. |
How to: Implement a Provider | Provides a step-by-step implementation of a provider for a temperature monitoring application. |
How to: Implement an Observer | Provides a step-by-step implementation of an observer for a temperature monitoring application. |
Collaborate with us on GitHub
The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see our contributor guide.