patron-observer

Observer Pattern: Notifications and Events

  • 5 min

The Observer is a pattern that notifies multiple subscribers when an object changes. The sender knows the observation contract, but doesn’t need to know the concrete classes that react.

We live in a world of notifications. When a YouTuber uploads a video, you get a notification on your phone. When someone tags you on Twitter, an alert pops up.

How does this work technically?

Imagine you want to know when the new iPhone goes on sale. You have two options:

  • Polling: ask the store every five minutes. This generates work even when there’s nothing new.
  • Push: ask to be notified when the product arrives.

The Observer pattern implements the second option. It defines a “one-to-many” dependency, so that when an object changes its state, it automatically notifies everyone who is “listening”.

The Problem: Maintaining Consistency

Imagine you have an Excel-like spreadsheet. You have data in column A and a bar chart representing that data.

If the user changes a value in cell A1, the chart must update immediately. If the code were coupled, the cell would have to know that a chart exists:

// ❌ Tight coupling
public void ChangeValue(int newValue)
{
    _value = newValue;
    _chart.Update(); // The cell knows about the chart!
    _pivotTable.Recalculate(); // And about the table!
}
Copied!

This is a disaster. If you add a third component tomorrow (e.g., a data validator), you have to modify the cell’s code.

The Solution: Publish / Subscribe

The Observer pattern breaks this coupling by separating two roles:

  1. Subject (Observable): The one that has the information (the Cell, the YouTuber). It maintains a list of subscribers.
  2. Observer: The one that wants to be notified (the Chart, the Subscriber).

When the Subject changes, it iterates through its list and notifies everyone: “Hey, I’ve changed!”. The Subject doesn’t care who is listening; it just knows it has a list of interested parties.

Classic Implementation in C# (Using Interfaces)

Let’s simulate a news channel.

The Interfaces

// The interface for the receiver of the news
public interface IObserver
{
    void Update(string news);
}

// The interface for the emitter of the news
public interface ISubject
{
    void Attach(IObserver observer); // Subscribe
    void Detach(IObserver observer); // Unsubscribe
    void Notify();                   // Notify everyone
}
Copied!

The Concrete Subject (News Agency)

public class NewsAgency : ISubject
{
    // List of subscribers
    private List<IObserver> _subscribers = new List<IObserver>();
    private string _latestNews;

    public void Attach(IObserver observer)
    {
        _subscribers.Add(observer);
        Console.WriteLine("Agency: New subscriber added.");
    }

    public void Detach(IObserver observer)
    {
        _subscribers.Remove(observer);
        Console.WriteLine("Agency: Subscriber removed.");
    }

    // Method that notifies everyone
    public void Notify()
    {
        Console.WriteLine("Agency: Notifying all subscribers...");
        foreach (var observer in _subscribers)
        {
            observer.Update(_latestNews);
        }
    }

    // Business logic that triggers the event
    public void PublishBreakingNews(string news)
    {
        Console.WriteLine($"\n📰 BULLETIN: {news}");
        _latestNews = news;
        Notify();
    }
}
Copied!

The Concrete Observers

We can have very different subscribers (TV, Radio, Newspaper) reacting to the same event in different ways.

public class TVChannel : IObserver
{
    private string _name;

    public TVChannel(string name) => _name = name;

    public void Update(string news)
    {
        Console.WriteLine($"📺 {_name} interrupts its broadcast: {news}");
    }
}

public class Radio : IObserver
{
    public void Update(string news)
    {
        Console.WriteLine($"📻 Radio: We're talking about the news '{news}'");
    }
}
Copied!

Running the Code

class Program
{
    static void Main(string[] args)
    {
        var agency = new NewsAgency();
        
        var tv1 = new TVChannel("Antena 3");
        var radio = new Radio();

        // 1. Subscription
        agency.Attach(tv1);
        agency.Attach(radio);

        // 2. Event
        agency.PublishBreakingNews("The cat climbed the tree");

        // 3. One unsubscribes
        agency.Detach(radio);

        // 4. New event
        agency.PublishBreakingNews("The cat came down from the tree");
    }
}
Copied!

Output:

Agency: New subscriber added.
Agency: New subscriber added.

📰 BULLETIN: The cat climbed the tree
Agency: Notifying all subscribers...
📺 Antena 3 interrupts its broadcast: The cat climbed the tree
📻 Radio: We're talking about the news 'The cat climbed the tree'

Agency: Subscriber removed.

📰 BULLETIN: The cat came down from the tree
Agency: Notifying all subscribers...
📺 Antena 3 interrupts its broadcast: The cat came down from the tree
Copied!

“Native” Implementation in C# (Events)

In C#, we rarely implement the Observer pattern “by hand” as shown above. The language has native support for it through delegates and the event keyword. This is syntactic sugar for the Observer pattern.

// SUBJECT
public class Button
{
    // Define the event (native Observer)
    public event EventHandler<string> Click;

    public void Press()
    {
        Console.WriteLine("\n🖱️ Button pressed.");
        // Invoke the subscribers if any exist
        Click?.Invoke(this, "The user clicked");
    }
}

// CLIENT
class Program
{
    static void Main(string[] args)
    {
        var button = new Button();

        // Subscription using += (Attach)
        button.Click += (sender, message) => Console.WriteLine($"Action 1: Save to DB ({message})");
        button.Click += (sender, message) => Console.WriteLine($"Action 2: Play sound");

        button.Press();
    }
}
Copied!

Observer and Reactive Extensions (Rx)

If we take this pattern to the extreme, we enter the world of Reactive Programming.

In .NET, there are interfaces like IObservable<T> and IObserver<T>. Libraries such as System.Reactive (Rx.NET) allow you to treat events as data streams.

Instead of simply reacting to an event, you can filter, transform, and combine events: “Notify me if the user clicks, but only if they haven’t clicked in the last second (Debounce) and if the button is the left one”.

This is essentially the Observer pattern with superpowers, combining Observer with Iterator.

When to Use Observer?

  • User Interfaces (UI): It’s the foundation of MVC/MVVM. The model changes and the view updates automatically.
  • Event Systems: When an action in one module must trigger processes in other, unknown modules.
  • Decoupling: When an object must notify others, but you don’t want it to know who those others are.

The Observer pattern is very useful for creating reactive and decoupled applications. It allows our objects to “talk” without needing to know each other intimately.