Understanding the Observer Design Pattern in C#

The Observer design pattern is a behavioral pattern that defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. This pattern is useful when an object should notify other objects without knowing who they are or how many of them exist.
Example without Observer Design Pattern
Let's consider a scenario where a stock market system needs to notify multiple clients (observers) whenever the stock prices (subject) change.
using System;
namespace WithoutObserverPattern
{
// Concrete class for Investor
class Investor
{
public string Name { get; private set; }
public Investor(string name)
{
Name = name;
}
public void Notify(string stockSymbol, float stockPrice)
{
Console.WriteLine($"Notified {Name} of {stockSymbol}'s change to {stockPrice}");
}
}
// Concrete class for Stock
class Stock
{
private string _symbol;
private float _price;
private Investor _investor1;
private Investor _investor2;
public Stock(string symbol, float price)
{
_symbol = symbol;
_price = price;
}
public void SetInvestor1(Investor investor)
{
_investor1 = investor;
}
public void SetInvestor2(Investor investor)
{
_investor2 = investor;
}
public void SetPrice(float price)
{
_price = price;
NotifyInvestors();
}
private void NotifyInvestors()
{
if (_investor1 != null)
{
_investor1.Notify(_symbol, _price);
}
if (_investor2 != null)
{
_investor2.Notify(_symbol, _price);
}
}
}
class Program
{
static void Main(string[] args)
{
Stock appleStock = new Stock("AAPL", 150.00f);
Investor investor1 = new Investor("John Doe");
Investor investor2 = new Investor("Jane Smith");
appleStock.SetInvestor1(investor1);
appleStock.SetInvestor2(investor2);
appleStock.SetPrice(155.00f);
appleStock.SetPrice(160.00f);
// Removing investor1
appleStock.SetInvestor1(null);
appleStock.SetPrice(165.00f);
}
}
}
Problems in the Non-Pattern Approach
Tight Coupling:The
Stock
class is tightly coupled to theInvestor
class. It has direct references toInvestor
instances, making it difficult to add or remove investors dynamically.Scalability Issues:The
Stock
class can only handle a fixed number of investors (in this case, two). Adding more investors requires modifying theStock
class, which is not scalable.Lack of Flexibility:The current approach lacks flexibility. Any change in the way investors are notified requires modifying the
Stock
class. This makes the code less flexible and harder to maintain.Single Responsibility Principle Violation:The
Stock
class is responsible for both managing stock data and notifying investors. This violates the Single Responsibility Principle, making the class harder to maintain.
How the Observer Pattern Solves These Problems
Loose Coupling:The Observer Pattern decouples the
Stock
class from theInvestor
class. TheStock
class only interacts with theIObserver
interface, making it easier to add or remove observers dynamically.Scalability:The Observer Pattern allows for an arbitrary number of observers. Observers can be added or removed at runtime without modifying the
Stock
class. This makes the system more scalable.Flexibility:The Observer Pattern provides a flexible mechanism to notify observers. Different types of observers can implement the
IObserver
interface, and the notification logic can be changed independently of theStock
class.Single Responsibility Principle:The
Stock
class is only responsible for managing stock data, while the responsibility of notifying observers is handled by the observer mechanism. This adheres to the Single Responsibility Principle, making the code easier to maintain.
Revisited Code with Observer Pattern
Here is how we can implement this pattern :
using System;
using System.Collections.Generic;
namespace ObserverPattern
{
// Observer interface
interface IObserver
{
void Update(string stockSymbol, float stockPrice);
}
// Concrete observer
class Investor : IObserver
{
public string Name { get; private set; }
public Investor(string name)
{
Name = name;
}
public void Update(string stockSymbol, float stockPrice)
{
Console.WriteLine($"Notified {Name} of {stockSymbol}'s change to {stockPrice}");
}
}
// Subject interface
interface IStock
{
void RegisterObserver(IObserver observer);
void RemoveObserver(IObserver observer);
void NotifyObservers();
}
// Concrete subject
class Stock : IStock
{
private List<IObserver> _observers;
private string _symbol;
private float _price;
public Stock(string symbol, float price)
{
_observers = new List<IObserver>();
_symbol = symbol;
_price = price;
}
public void RegisterObserver(IObserver observer)
{
_observers.Add(observer);
}
public void RemoveObserver(IObserver observer)
{
_observers.Remove(observer);
}
public void NotifyObservers()
{
foreach (var observer in _observers)
{
observer.Update(_symbol, _price);
}
}
public void SetPrice(float price)
{
_price = price;
NotifyObservers();
}
}
class Program
{
static void Main(string[] args)
{
Stock appleStock = new Stock("AAPL", 150.00f);
IObserver investor1 = new Investor("John Doe");
IObserver investor2 = new Investor("Jane Smith");
appleStock.RegisterObserver(investor1);
appleStock.RegisterObserver(investor2);
appleStock.SetPrice(155.00f);
appleStock.SetPrice(160.00f);
appleStock.RemoveObserver(investor1);
appleStock.SetPrice(165.00f);
}
}
}
Why Can't We Use Other Design Patterns Instead?
Strategy Pattern: The Strategy pattern defines a family of interchangeable algorithms and allows the client to choose which algorithm to use. It does not involve notifying multiple dependent objects of state changes.
Template Method Pattern: The Template Method pattern defines the structure of an algorithm but allows subclasses to override certain steps. It is not designed for notifying multiple dependent objects.
Chain of Responsibility Pattern: The Chain of Responsibility pattern allows a request to be passed along a chain of handlers until one of them handles the request. It is not suitable for scenarios where one object needs to notify multiple dependent objects of state changes.
Steps to Identify Use Cases for the Observer Pattern
Identify Dependent Objects: Look for scenarios where multiple objects depend on the state of another object.
Decouple State Changes and Notifications: Ensure that state changes in the subject should automatically trigger notifications to its observers without tight coupling.
Support Dynamic Relationships: The pattern should support adding or removing observers at runtime based on dynamic conditions.
Promote Maintainability and Scalability: Consider the Observer pattern when you want to promote maintainability by separating state change logic from notification logic and ensure scalability for systems with many dependent objects.
By following these steps and implementing the Observer pattern, you can achieve decoupling between the subject and its observers, providing dynamic, flexible, and scalable notification mechanisms in your system.