Understanding the Observer Pattern in C#

Published on: February 24, 2025

The Problem: Reacting to Game Events

Imagine you’re developing a fantasy RPG game. In your game, different characters (a Knight, a Mage, and a Merchant) react to world events like dragon attacks or gold discoveries.

How do we ensure that all these characters get notified when something important happens? One way is to manually call update methods on each character whenever an event occurs. Let’s see how that looks in code:

public class DragonEventManager {
    private Knight knight;
    private Mage mage;
    private Merchant merchant;
    private int dragonPower;

    public DragonEventManager(Knight knight, Mage mage, Merchant merchant) {
        this.knight = knight;
        this.mage = mage;
        this.merchant = merchant;
    }

    public void DragonAttacks(int power) {
        Console.WriteLine("A dragon appears!");
        dragonPower = power;
        knight.React();
        mage.React();
        merchant.React();
    }
}

public class Knight {
    public void React() {
        Console.WriteLine("Knight draws his sword!");
    }
}

public class Mage {
    public void React() {
        Console.WriteLine("Mage casts a fireball!");
    }
}

public class Merchant {
    public void React() {
        Console.WriteLine("Merchant hides behind a barrel!");
    }
}

Checkout this code here

At first, this seems fine. But what if we want to add a new character who reacts to the event? We have to modify the DragonEventManager class every time! This tightly couples our components, making the system hard to maintain.

Enter the Observer Pattern

The Observer Pattern helps us decouple things. Instead of DragonEventManager keeping track of who needs to react, we let characters subscribe to events. The event manager simply notifies all subscribers when an event happens.

Let’s refactor our code step by step.

Step 1: Define an Interface for Observers

We create an interface that any character can implement if they want to react to game events.

public interface IObserver {
    void React();
}

Now, our Knight, Mage, and Merchant classes will implement this interface.

public class Knight : IObserver {
    public void React() {
        Console.WriteLine("Knight draws his sword!");
    }
}

public class Mage : IObserver {
    public void React() {
        Console.WriteLine("Mage casts a fireball!");
    }
}

public class Merchant : IObserver {
    public void React() {
        Console.WriteLine("Merchant hides behind a barrel!");
    }
}

Step 2: Make DragonEventManager an Observable Subject

Now, we modify DragonEventManager to keep a list of observers and notify them when an event happens.

using System.Collections.Generic;

public class DragonEventManager {
    private List<IObserver> observers = new List<IObserver>();
    private int dragonPower;

    public void AddObserver(IObserver observer) {
        observers.Add(observer);
    }

    public void RemoveObserver(IObserver observer) {
        observers.Remove(observer);
    }

    public void DragonAttacks(int power) {
        Console.WriteLine("A dragon appears!");
        dragonPower = power;
        NotifyObservers();
    }

    private void NotifyObservers() {
        foreach (var observer in observers) {
            observer.React();
        }
    }

    public int GetDragonPower() {
        return dragonPower;
    }
}

Step 3: Updating the Mage to Retrieve Dragon Power

Previously, our Mage just reacted to the attack, but now it can retrieve more details about the dragon.

public class Mage : IObserver {
    private DragonEventManager _eventManager;

    public Mage(DragonEventManager eventManager) {
        _eventManager = eventManager;
    }

    public void React() {
        Console.WriteLine("Mage checks the dragon's power level...");
        int dragonPower = _eventManager.GetDragonPower();
        if (dragonPower > 50) {
            Console.WriteLine("Mage casts a powerful fireball!");
        } else {
            Console.WriteLine("Mage uses a simple magic bolt.");
        }
    }
}

Step 4: Putting It All Together

Now, our system is flexible! We can dynamically add or remove characters at runtime.

class Program {
    static void Main() {
        DragonEventManager eventManager = new DragonEventManager();
        Knight knight = new Knight();
        Mage mage = new Mage(eventManager);
        Merchant merchant = new Merchant();

        eventManager.AddObserver(knight);
        eventManager.AddObserver(mage);
        eventManager.AddObserver(merchant);

        eventManager.DragonAttacks(60);
    }
}

Check this code here

The Benefits

  • Decoupling: The DragonEventManager doesn’t need to know who is reacting.
  • Scalability: Adding new characters is easy. Just implement IObserver and subscribe!
  • Flexibility: We can remove observers dynamically without modifying DragonEventManager.

Why We Introduced DragonEventManager

Before, DragonEventManager had direct dependencies on characters, making it hard to extend. Now, by using the Observer Pattern, characters subscribe dynamically, making the system more maintainable and reusable.

Now go ahead and try it yourself! Maybe implement a thief character who only reacts when gold is discovered. Get creative!


Happy coding! ⚡


Design PatternsObserver PatternC#OOPSoftware Engineering