Skip to main content

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

  1. Tight Coupling:The Stock class is tightly coupled to the Investor class. It has direct references to Investor instances, making it difficult to add or remove investors dynamically.

  2. Scalability Issues:The Stock class can only handle a fixed number of investors (in this case, two). Adding more investors requires modifying the Stock class, which is not scalable.

  3. 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.

  4. 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
  1. Loose Coupling:The Observer Pattern decouples the Stock class from the Investor class. The Stock class only interacts with the IObserver interface, making it easier to add or remove observers dynamically.

  2. 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.

  3. 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 the Stock class.

  4. 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

  1. Identify Dependent Objects: Look for scenarios where multiple objects depend on the state of another object.
  2. Decouple State Changes and Notifications: Ensure that state changes in the subject should automatically trigger notifications to its observers without tight coupling.
  3. Support Dynamic Relationships: The pattern should support adding or removing observers at runtime based on dynamic conditions.
  4. 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.

Comments

Popular posts from this blog

Implementing and Integrating RabbitMQ in .NET Core Application: Shopping Cart and Order API

RabbitMQ is a robust message broker that enables communication between services in a decoupled, reliable manner. In this guide, we’ll implement RabbitMQ in a .NET Core application to connect two microservices: Shopping Cart API (Producer) and Order API (Consumer). 1. Prerequisites Install RabbitMQ locally or on a server. Default Management UI: http://localhost:15672 Default Credentials: guest/guest Install the RabbitMQ.Client package for .NET: dotnet add package RabbitMQ.Client 2. Architecture Overview Shopping Cart API (Producer): Sends a message when a user places an order. RabbitMQ : Acts as the broker to hold the message. Order API (Consumer): Receives the message and processes the order. 3. RabbitMQ Producer: Shopping Cart API Step 1: Install RabbitMQ.Client Ensure the RabbitMQ client library is installed: dotnet add package RabbitMQ.Client Step 2: Create the Producer Service Add a RabbitMQProducer class to send messages. RabbitMQProducer.cs : using RabbitMQ.Client; usin...

How Does My .NET Core Application Build Once and Run Everywhere?

One of the most powerful features of .NET Core is its cross-platform nature. Unlike the traditional .NET Framework, which was limited to Windows, .NET Core allows you to build your application once and run it on Windows , Linux , or macOS . This makes it an excellent choice for modern, scalable, and portable applications. In this blog, we’ll explore how .NET Core achieves this, the underlying architecture, and how you can leverage it to make your applications truly cross-platform. Key Features of .NET Core for Cross-Platform Development Platform Independence : .NET Core Runtime is available for multiple platforms (Windows, Linux, macOS). Applications can run seamlessly without platform-specific adjustments. Build Once, Run Anywhere : Compile your code once and deploy it on any OS with minimal effort. Self-Contained Deployment : .NET Core apps can include the runtime in the deployment package, making them independent of the host system's installed runtime. Standardized Libraries ...

Clean Architecture: What It Is and How It Differs from Microservices

In the tech world, buzzwords like   Clean Architecture   and   Microservices   often dominate discussions about building scalable, maintainable applications. But what exactly is Clean Architecture? How does it compare to Microservices? And most importantly, is it more efficient? Let’s break it all down, from understanding the core principles of Clean Architecture to comparing it with Microservices. By the end of this blog, you’ll know when to use each and why Clean Architecture might just be the silent hero your projects need. What is Clean Architecture? Clean Architecture  is a design paradigm introduced by Robert C. Martin (Uncle Bob) in his book  Clean Architecture: A Craftsman’s Guide to Software Structure and Design . It’s an evolution of layered architecture, focusing on organizing code in a way that makes it  flexible ,  testable , and  easy to maintain . Core Principles of Clean Architecture Dependency Inversion : High-level modules s...