Skip to Content
Course content

50: Practice Exercise: Building a Simple Publish-Subscribe System

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was working on a retail backend where the OrderProcessingService had become a total nightmare. Every time a customer placed an order, that one class was responsible for charging the credit card, updating the inventory, sending a confirmation email, and notifying the shipping warehouse. It was a "God Object." Whenever the marketing team wanted to add a new "Welcome" SMS, I had to open that massive service, add another dependency, and risk breaking the payment logic just to send a text message. It was brittle, terrifying to test, and a classic example of tight coupling.

The solution was to move to a Publish-Subscribe (Pub-Sub) model. Instead of the OrderProcessingService knowing exactly who needs to react to an order, it simply shouts into a void: "An order was placed!" Any other part of the system that cares about that event—the EmailService, the InventoryService, the SMSGateway—simply listens for that specific shout and reacts accordingly. The publisher doesn't know the subscribers exist, and that's where the magic happens.

Decoupling with a Generic Message Bus

In C#, we could use built-in events, but for a truly decoupled system, creating a central "Message Bus" is often cleaner. The goal is to create a system where any object can publish a message of any type, and any other object can subscribe to that specific type. I usually implement this using a Dictionary where the key is the Type of the message and the value is a list of delegates (actions) to execute when that message arrives.

The trick here is using Generics. You want your Subscribe method to take a T and an Action<T>. Since the dictionary stores these as a general Delegate or object, you'll have to do a bit of casting when the message is published. It feels a little "unsafe" at first, but when wrapped in a type-safe generic method, the consumer of the bus never sees the casting logic.

Managing the Subscription Lifecycle

One thing I've learned the hard way is that Pub-Sub systems can lead to memory leaks if you aren't careful. If a short-lived object subscribes to a long-lived Message Bus, the bus keeps a reference to that object, preventing the Garbage Collector from cleaning it up. This is the "Lapsed Listener" problem.

To fix this, your system needs an Unsubscribe method. In a professional environment, I'd probably use WeakReference or return a disposable token, but for a simple implementation, explicitly removing the action from the internal list is the way to go. When you're building this, think about how you'll identify which subscriber to remove—usually, passing the same action reference back to the bus is the simplest approach.

// A conceptual glimpse of the pattern
public class SimplePubSub
{
    private readonly Dictionary<Type, List<Delegate>> _subscriptions = new();

    public void Subscribe<T>(Action<T> handler)
    {
        var type = typeof(T);
        if (!_subscriptions.ContainsKey(type)) 
            _subscriptions[type] = new List<Delegate>();
        
        _subscriptions[type].Add(handler);
    }

    public void Publish<T>(T message)
    {
        var type = typeof(T);
        if (_subscriptions.ContainsKey(type))
        {
            foreach (var handler in _subscriptions[type])
            {
                ((Action<T>)handler)(message);
            }
        }
    }
}



📋 Practical Task

Exercise: The RPG Game Event System

You are building the core event infrastructure for a Role-Playing Game (RPG). Instead of the Player class manually calling methods on the UI and the Quest Log, you will implement a Pub-Sub system to handle game events.

Requirements:

  • Create a GameEventBus class that allows any type of message to be published and subscribed to.
  • Define two message types (as simple classes or records): PlayerDamagedEvent (containing the amount of damage) and QuestCompletedEvent (containing the name of the quest).
  • Create a HealthBarUI class that subscribes to PlayerDamagedEvent and prints "UI: Updating health bar by [X] points" to the console.
  • Create a QuestTracker class that subscribes to QuestCompletedEvent and prints "Quest Tracker: Marking [Quest Name] as complete!" to the console.
  • In your Main method:
    1. Instantiate the bus and the two subscriber classes.
    2. Register the subscribers to the bus.
    3. Publish a PlayerDamagedEvent and a QuestCompletedEvent.
    4. Unsubscribe the HealthBarUI and publish another PlayerDamagedEvent to verify it no longer reacts.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.