.NET Framework, Software Development

Understanding the Observer Pattern in .NET

The Observer pattern is a behavioral design pattern that promotes a one-to-many dependency between objects. The main purpose of this pattern is to ensure that when one object (the subject) changes state, all of its dependents (observers) are notified and updated automatically.

In the .NET ecosystem, the Observer pattern has been implemented and can be utilized via the IObservable<T> and IObserver<T> interfaces. This article will focus on understanding and implementing this pattern from a .NET-centric perspective.

Basic Concepts:

  1. Subject (Observable): Represents the entity that maintains a collection of observers and facilitates adding or removing observers. Whenever a state change occurs, the Observable notifies all its observers.
  2. Observers: These are the entities that need to be updated when the Observable changes. They subscribe to the Observable and react to its notifications.

.NET’s IObservable<T> and IObserver<T>:

.NET provides native support for the Observer pattern through these interfaces. The IObservable<T> interface represents the Observable while the IObserver<T> interface represents the Observer.

  • IObservable<T>: Contains a single method, Subscribe, that observers call to register themselves and start receiving notifications.
  • IObserver<T>: Contains three methods that handle notifications from the Observable:
    • OnNext(T value): Notifies the observer that the provider has produced data.
    • OnError(Exception error): Notifies the observer that the provider has encountered an error.
    • OnCompleted(): Notifies the observer that the provider has finished sending notifications.

Implementation Example:

Let’s consider a simple example where an Stock class (Observable) notifies its subscribers (Observers) when its price changes.

// The Observable
public class Stock : IObservable<decimal>
{
    private List<IObserver<decimal>> _observers = new List<IObserver<decimal>>();
    private decimal _price;

    public decimal Price
    {
        get => _price;
        set
        {
            _price = value;
            NotifyObservers();
        }
    }

    public IDisposable Subscribe(IObserver<decimal> observer)
    {
        if (!_observers.Contains(observer))
            _observers.Add(observer);
        
        return new Unsubscriber(_observers, observer);
    }

    private void NotifyObservers()
    {
        foreach (var observer in _observers)
        {
            observer.OnNext(_price);
        }
    }

    // Nested class to handle unsubscribing
    private class Unsubscriber : IDisposable
    {
        private List<IObserver<decimal>> _observers;
        private IObserver<decimal> _observer;

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

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

// The Observer
public class StockObserver : IObserver<decimal>
{
    public void OnNext(decimal value)
    {
        Console.WriteLine($"Notified of stock price change: {value}");
    }

    public void OnError(Exception error)
    {
        Console.WriteLine($"Error encountered: {error.Message}");
    }

    public void OnCompleted()
    {
        Console.WriteLine("End of notifications.");
    }
}

// Client code
var stock = new Stock();
var observer = new StockObserver();
using (stock.Subscribe(observer))
{
    stock.Price = 100.5m;
    stock.Price = 102.5m;
}

Benefits:

  1. Decoupling: The Observer pattern ensures that the Observable and Observers are decoupled. This means that changes in one do not directly impact the other.
  2. Dynamic Relationships: Observers can subscribe and unsubscribe from Observables dynamically during runtime.
  3. Reusability: Both Observers and Observables can be reused in different parts of an application or even across multiple applications.

The Observer pattern in .NET provides an elegant way to keep multiple parts of an application in sync. By understanding the role of IObservable<T> and IObserver<T>, developers can effectively implement dynamic, decoupled systems that respond promptly to changes in state.