Type Behavioral
Purpose Define a one-to-many dependency between objects so that when one object changes its state, all its dependencies are notified and updated automatically.

Description

The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all of its dependants are notified and updated automatically.

It is commonly used in systems where a change in one object may need to be reflected across several others, without tightly coupling those objects together. Instead of the subject needing to know the details of every dependant, observers subscribe to it and are notified when relevant changes occur.

Observer is closely related to publisher-subscriber style communication, although the two are not necessarily identical in structure or intent.

Observer is primarily concerned with notifying dependants when state changes.


Overview

Observer
Subject Defines the operations used to register, remove, and notify observers.
ConcreteSubject This is the concrete object that implements Subject. It maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods.
Observer Interface the ConcreteObservers implement and the Subject uses to notify them of updates.
ConcreteObservers These are the objects that need to keep track of the subject. Observers register themselves with the subject if they want to be notified of any change in the subject's state.

First Implementation

This first example is a more classic implementation in C# that you'll see in older code bases:

public interface IObserver
{
    void Update();
}

public class ConcreteObserver(string name) : IObserver
{
    public void Update() => Console.WriteLine($"\t{name} notified of state change.");
}

public class Subject
{
    private readonly List<IObserver> _observers = [];

    public void Attach(IObserver observer) => _observers.Add(observer);

    public void Detach(IObserver observer) => _observers.Remove(observer);

    public void Notify()
    {
        Console.WriteLine("Subject notifying observers:");
        
        foreach (var observer in _observers)
        {
            observer.Update();
        }
    }
}

public static class ObserverDemo
{
    public static void Execute()
    {
        var subject = new Subject();
        
        IObserver observer1 = new ConcreteObserver("Observer 1");
        IObserver observer2 = new ConcreteObserver("Observer 2");
        
        subject.Attach(observer1);
        subject.Attach(observer2);
        subject.Notify(); 
        
        subject.Detach(observer2);
        subject.Notify();
        
        subject.Detach(observer1);
        subject.Notify();
    }
}

// outputs:

// Subject notifying observers:
//      Observer 1 notified of state change.
//      Observer 2 notified of state change.
// Subject notifying observers:
//      Observer 1 notified of state change.
// Subject notifying observers:


Second Implementation

This is a simpler example, leveraging more modern C# features:

public record SecurityGuard(string Name)
{
    public static readonly SecurityGuard None = new("");
}

// ConcreteObserver and Observer rolled into one

public class Thief(string name)
{
    public void OnGuardChange(SecurityGuard guard)
    {
        Console.WriteLine(guard == SecurityGuard.None
            ? $"{name} stirs, sees no guard on duty and becomes sneaky."
            : $"{name} stirs, sees {guard.Name} on duty and goes back to bed.");
    }
}

// Subject

public class MountDoom
{
    private SecurityGuard _guard = SecurityGuard.None;

    public SecurityGuard Guard
    {
        get => _guard;
        set
        {
            _guard = value;
            OnGuardChange(_guard);
        }
    }
    
    public event Action<SecurityGuard> OnGuardChange = delegate { };
}

// Demonstration

public static class ObserverDemo
{
    public static void Execute()
    {
        var location = new MountDoom();
        
        var frodo = new Thief("Frodo");
        var samwise = new Thief("Samwise");        
        var gollum = new SecurityGuard("Gollum");
        
        location.OnGuardChange += frodo.OnGuardChange;
        location.OnGuardChange += samwise.OnGuardChange;
        location.Guard = gollum;
        
        location.OnGuardChange -= samwise.OnGuardChange;
        location.Guard = SecurityGuard.None;
    }
}

// outputs:

// Frodo stirs, sees Gollum on duty and goes back to bed.
// Samwise stirs, sees Gollum on duty and goes back to bed.
// Frodo stirs, sees no guard on duty and becomes sneaky.

Summary

Robert Nystrom has a nice chapter online here.

For more information, please see here.