Observer Pattern
Definitionβ
The Observer defines a one-to-many dependency between objects so that when one object (the subject) changes state, all its dependents (observers) are notified and updated automatically.
Coffee Shop Exampleβ
When a customer places an order, multiple systems need to react: the kitchen display shows the new order, the loyalty system awards points, the inventory deducts stock, and the customer gets a notification. Instead of the order knowing about every system, it simply notifies its observers.
Structureβ
Observer Interfaceβ
public record Order(string Customer, string Coffee, CupSize Size, decimal Price);
public interface IOrderObserver
{
void OnOrderPlaced(Order order);
void OnOrderReady(Order order);
}
Subject β The Order Systemβ
public class OrderSubject
{
private readonly List<IOrderObserver> _observers = new();
public void Subscribe(IOrderObserver observer)
{
if (!_observers.Contains(observer))
_observers.Add(observer);
}
public void Unsubscribe(IOrderObserver observer) =>
_observers.Remove(observer);
public void NotifyOrderPlaced(Order order)
{
Console.WriteLine($"\nπ¦ New order: {order.Customer}'s {order.Size} {order.Coffee}");
foreach (var observer in _observers)
observer.OnOrderPlaced(order);
}
public void NotifyOrderReady(Order order)
{
Console.WriteLine($"\nβ Order ready: {order.Customer}'s {order.Size} {order.Coffee}");
foreach (var observer in _observers)
observer.OnOrderReady(order);
}
}
Concrete Observersβ
public class KitchenDisplay : IOrderObserver
{
public void OnOrderPlaced(Order order) =>
Console.WriteLine($" [Kitchen] β Brewing {order.Size} {order.Coffee} for {order.Customer}");
public void OnOrderReady(Order order) =>
Console.WriteLine($" [Kitchen] β
{order.Coffee} for {order.Customer} is ready for pickup");
}
public class LoyaltyTracker : IOrderObserver
{
private readonly Dictionary<string, int> _points = new();
public void OnOrderPlaced(Order order)
{
var earned = (int)(order.Price * 10);
_points[order.Customer] = _points.GetValueOrDefault(order.Customer) + earned;
Console.WriteLine($" [Loyalty] {order.Customer} earned +{earned} pts (total: {_points[order.Customer]})");
}
public void OnOrderReady(Order order) { } // Not interested in ready events
}
public class InventoryTracker : IOrderObserver
{
public void OnOrderPlaced(Order order) =>
Console.WriteLine($" [Inventory] Deducted supplies for {order.Coffee}");
public void OnOrderReady(Order order) { } // Not interested in ready events
}
public class CustomerNotifier : IOrderObserver
{
public void OnOrderPlaced(Order order) =>
Console.WriteLine($" [SMS] Hi {order.Customer}, your {order.Coffee} is being prepared!");
public void OnOrderReady(Order order) =>
Console.WriteLine($" [SMS] Hi {order.Customer}, your {order.Coffee} is ready! π");
}
Client β Wiring It Upβ
var orderSystem = new OrderSubject();
// Subscribe all observers
orderSystem.Subscribe(new KitchenDisplay());
orderSystem.Subscribe(new LoyaltyTracker());
orderSystem.Subscribe(new InventoryTracker());
orderSystem.Subscribe(new CustomerNotifier());
// Place and complete orders
orderSystem.NotifyOrderPlaced(new Order("Alice", "Latte", CupSize.Large, 4.50m));
orderSystem.NotifyOrderReady(new Order("Alice", "Latte", CupSize.Large, 4.50m));
orderSystem.NotifyOrderPlaced(new Order("Bob", "Espresso", CupSize.Small, 2.50m));
orderSystem.NotifyOrderReady(new Order("Bob", "Espresso", CupSize.Small, 2.50m));
// Output:
// π¦ New order: Alice's Large Latte
// [Kitchen] β Brewing Large Latte for Alice
// [Loyalty] Alice earned +45 pts (total: 45)
// [Inventory] Deducted supplies for Latte
// [SMS] Hi Alice, your Latte is being prepared!
//
// β Order ready: Alice's Large Latte
// [Kitchen] β
Latte for Alice is ready for pickup
// [SMS] Hi Alice, your Latte is ready! π
The OrderSubject doesn't know what the observers do β only that they implement IOrderObserver. Adding a new observer (e.g., analytics logger) requires zero changes to the order system (OCP).
.NET Built-In Observer: Eventsβ
C# has Observer built into the language via events and delegates:
// Modern C# approach using events
public class CoffeeOrderSystem
{
// Define the event
public event EventHandler<OrderEventArgs>? OrderPlaced;
public event EventHandler<OrderEventArgs>? OrderReady;
}
public record OrderEventArgs(Order Order) : EventArgs;
// Subscribing
var system = new CoffeeOrderSystem();
system.OrderPlaced += (sender, args) =>
Console.WriteLine($"[Kitchen] Brewing {args.Order.Coffee}");
system.OrderReady += (sender, args) =>
Console.WriteLine($"[SMS] {args.Order.Coffee} is ready!");
// Raising
system.OrderPlaced?.Invoke(system, new OrderEventArgs(order));
IObservable<T> β Reactive Extensionsβ
.NET has a formal IObservable<T> / IObserver<T> interface pair:
// Using System.Reactive (Rx.NET)
var orderStream = new Subject<Order>();
orderStream.Subscribe(order =>
Console.WriteLine($"[Kitchen] {order.Coffee}"));
orderStream.Subscribe(order =>
Console.WriteLine($"[Loyalty] +{(int)(order.Price * 10)} pts"));
orderStream.OnNext(new Order("Alice", "Latte", CupSize.Large, 4.50m));
Observer vs Mediatorβ
| Observer | Mediator | |
|---|---|---|
| Communication | Subject β Observers (broadcast) | Colleagues β Mediator (two-way) |
| Coupling | Observers know about the subject | Colleagues only know the mediator |
| Knowledge | Subject doesn't know who observes it | Mediator knows all colleagues |
| Complexity | Simple one-to-many | Centralized multi-way communication |
.NET Real-World Usageβ
event/EventHandlerβ built-in Observer pattern in C#INotifyPropertyChangedβ WPF/MAUI data bindingIObservable<T>/IObserver<T>β Rx.NET reactive programming- SignalR β real-time notifications to connected clients
IProgress<T>β progress reporting without coupling
When to Useβ
- A change to one object requires changing others, and you don't know exactly how many
- An object should notify others without being tightly coupled to them
- You need event-driven architecture (pub/sub)
When NOT to Useβ
- Only one observer β a direct callback is simpler
- The update chain becomes unpredictable β observers triggering observers causing infinite loops
- Performance is critical and notification overhead matters
Key Takeawaysβ
- Observer establishes a one-to-many relationship with automatic notification
- Subject and observers are loosely coupled β subject doesn't know observer types
- C# provides built-in support via events and
IObservable<T> - Always unsubscribe to prevent memory leaks
Interview Questionsβ
Q: What's the difference between Observer and Pub/Sub? Observer is direct β the subject calls observer methods. Pub/Sub introduces a message broker between publisher and subscriber, making them completely decoupled. Observer is synchronous by default; Pub/Sub is typically asynchronous.
Q: How do you prevent memory leaks in Observer?
Observers hold a reference to the subject (via the event). If the observer isn't unsubscribed, the GC can't collect it. Always call -= to unsubscribe, or use weak reference patterns (WeakEventManager in WPF).
Q: How does IObservable<T> differ from IEnumerable<T>?
IEnumerable<T> is pull-based β you iterate to get items. IObservable<T> is push-based β items come to you as they're produced. They're duals of each other (duality principle in reactive programming).
Related Topicsβ
- Mediator β centralized communication between colleagues
- Strategy β different intent (swap algorithms), similar composition
- Delegates & Events β C# language features for Observer
- SOLID β OCP & DIP β new observers don't break existing code