Observer pattern in C#

 Hello C# lovers! 😊

In this article, I will explain what the observer pattern is and how to use it in C#. I will also show you some examples of how to implement the observer pattern using events and interfaces. 🚀

The observer pattern is a behavioral design pattern that allows an object (called the subject or the provider) to notify other objects (called the observers or the subscribers) about changes in its state. The observers can then react to these changes in a suitable way. 😍

The observer pattern is useful for scenarios where you need to implement a push-based notification system, where the provider pushes updates to the observers whenever something interesting happens. For example, you can use the observer pattern to:

  • Update a user interface when the data source changes.
  • Send alerts when a sensor detects some event.
  • Monitor the price of a cryptocurrency and execute trades.

The observer pattern has many benefits, such as:

  • It supports loose coupling between the provider and the observers. The provider does not need to know anything about the observers, and the observers do not need to know anything about the provider’s implementation details.
  • It allows for dynamic subscription and unsubscription of observers. The observers can register and unregister themselves from receiving notifications at any time.
  • It allows for broadcasting messages to multiple observers. The provider can notify any number of observers with a single method call.

However, the observer pattern also has some drawbacks, such as:

  • It can cause memory leaks if the observers are not properly unsubscribed from the provider. The provider may keep references to the observers that are no longer needed, preventing them from being garbage collected.
  • It can cause unexpected side effects if the observers perform complex or time-consuming operations in response to notifications. The provider may block or slow down while waiting for the observers to finish their work.
  • It can cause concurrency issues if the provider and the observers run on different threads. The provider may notify the observers before it has finished updating its state, or the observers may access shared resources without proper synchronization.

To implement the observer pattern in C#, we have two options: using events or using interfaces.

Using Events

Events are a feature of C# that allows an object to expose a special kind of delegate (called an event handler) that can be subscribed by other objects. Events are based on the publisher-subscriber model, where the publisher raises an event and the subscribers handle it.

Events are a natural way to implement the observer pattern in C#, because they provide a simple and elegant syntax for registering and unregistering observers, as well as raising and handling notifications.

To use events for the observer pattern, we need to follow these steps:

  • Define an event handler delegate that specifies the signature of the method that handles the event. The delegate usually takes two parameters: an object that represents the event's sender, and an EventArgs object that contains additional information about the event.
  • Define an event in the provider class that uses the event handler delegate. The event acts as a field that stores a list of references to methods that handle it.
  • Define a method in the provider class that raises (or invokes) the event. The method usually checks if there are any subscribers to the event, and then calls each subscriber’s method with appropriate arguments.
  • Define one or more methods in the observer classes that match the signature of the event handler delegate. These methods contain the logic that responds to notifications from the provider.
  • Subscribe and unsubscribe from the event using += and -= operators. The subscribers can add or remove their methods from the event’s invocation list at any time.

Let’s see an example of how to use events for implementing the observer pattern.

Suppose we want to create a simple app that monitors the price of Bitcoin (BTC) and notifies the users when the price changes. We can use the observer pattern to implement this app using events.

First, we need to define an event handler delegate that specifies the signature of the method that handles the price change event. We can use a generic delegate called EventHandler<T> that takes a sender object and an EventArgs object as parameters. We can also create a custom class that derives from EventArgs and contains a property for the new price. 

For example:


// A custom class that contains the new price
public class PriceChangedEventArgs : EventArgs
{
    public decimal NewPrice { get; }

    public PriceChangedEventArgs(decimal newPrice)
    {
        NewPrice = newPrice;
    }
}

// An event handler delegate that uses the custom class
public delegate void PriceChangedEventHandler(object sender, PriceChangedEventArgs e);

Next, we need to define an event in the provider class that uses the event handler delegate. The provider class is responsible for fetching the current price of BTC from some external source (such as an API) and raising the event when the price changes. 

For example:



// A provider class that monitors the price of BTC
public class BtcPriceMonitor { // An event that uses the event handler delegate public event PriceChangedEventHandler PriceChanged; // A field that stores the current price private decimal currentPrice; // A method that fetches the current price from an API private decimal GetCurrentPrice() { // Some logic to call an API and get the current price // For simplicity, we use a random number here return new Random().Next(30000, 60000); } // A method that raises the event private void OnPriceChanged(PriceChangedEventArgs e) { // Check if there are any subscribers if (PriceChanged != null) { // Call each subscriber's method PriceChanged(this, e); } } // A method that checks for price changes periodically public async Task CheckPriceAsync() { while (true) { // Get the current price from the API var newPrice = GetCurrentPrice(); // Compare with the previous price if (newPrice != currentPrice) { // Update the current price currentPrice = newPrice; // Create a custom EventArgs object with the new price var args = new PriceChangedEventArgs(newPrice); // Raise the event OnPriceChanged(args); } // Wait for some time before checking again await Task.Delay(5000); } } }

Then, we need to define one or more methods in the observer classes that match the signature of the event handler delegate. The observer classes are responsible for displaying or logging or processing the notifications from the provider. 

For example:


// An observer class that displays the price change on console
public class ConsoleDisplay
{
    // A method that matches the signature of the event handler delegate
    public void Display(object sender, PriceChangedEventArgs e)
    {
        // Display the new price on console
        Console.WriteLine($"The new BTC price is {e.NewPrice:C}");
    }
}

// An observer class that logs the price change to a file
public class FileLogger
{
    // A method that matches the signature of the event handler delegate
    public void Log(object sender, PriceChangedEventArgs e)
    {
        // Log the new price to a file
        File.AppendAllText("log.txt", $"The new BTC price is {e.NewPrice:C}\n");
    }
}

Finally, we need to subscribe and unsubscribe from the event using += and -= operators. The subscribers can add or remove their methods from the event’s invocation list at any time. 

For example:


// Create an instance of the provider class
var monitor = new BtcPriceMonitor();

// Create instances of the observer classes
var display = new ConsoleDisplay();
var logger = new FileLogger();

// Subscribe to the event using += operator
monitor.PriceChanged += display.Display;
monitor.PriceChanged += logger.Log;

// Start checking for price changes asynchronously
await monitor.CheckPriceAsync();

// Unsubscribe from the event using -= operator (optional)
monitor.PriceChanged -= display.Display;
monitor.PriceChanged -= logger.Log;

That’s it! We have implemented the observer pattern using events in C#.

Using events for implementing the observer pattern is very convenient and easy in C#, because events are built-in features of the language and provide a clear and consistent syntax for working with them. However, events also have some limitations, such as:

  • Events are not generic. They only support one type of notification data per event. If you need to support multiple types of data or notifications, you need to define multiple events or use some workaround.
  • Events are not flexible. They only support one way of communication between providers and observers. If you need to support bidirectional communication or complex interactions, you need to use some additional mechanism.
  • Events are not discoverable. They do not expose any metadata or information about themselves or their subscribers. If you need to inspect or manipulate events or their subscribers at runtime, you need to use some reflection or custom logic.

Using Interfaces

Interfaces are another option for implementing the observer pattern in C#. Interfaces are contracts that define what methods or properties a class must implement. Interfaces can be used to decouple providers and observers by defining common interfaces for them.

To use interfaces for implementing the observer pattern in C#, we have two options: using custom interfaces or using built-in interfaces.

Using Custom Interfaces

Using custom interfaces means defining our own interfaces for providers and observers and implementing them in our classes. This gives us more control and flexibility over how we implement and use the observer pattern.

To use custom interfaces for implementing the observer pattern in C#, we need to follow these steps:

  • Define an interface for providers that specifies a method for registering observers and a method for notifying observers.
  • Define an interface for observers that specifies a method for handling notifications from providers.
  • Implement these interfaces in our provider and observer classes.
  • Use these interfaces to register and notify observers.

Let’s see an example of how to use custom interfaces for implementing the observer pattern.

We will use the same scenario as before: creating a simple app that monitors the price of Bitcoin (BTC) and notifies the users when the price changes. We will use custom interfaces to implement the observer pattern.

First, we need to define an interface for providers that specifies a method for registering observers and a method for notifying observers. 

For example:


// An interface for providers
public interface IBtcPriceProvider
{
    // A method for registering observers
    void RegisterObserver(IBtcPriceObserver observer);

    // A method for notifying observers
    void NotifyObservers(decimal newPrice);
}

Next, we need to define an interface for observers that specifies a method for handling notifications from providers. 

For example:


// An interface for observers
public interface IBtcPriceObserver
{
    // A method for handling notifications
    void Update(decimal newPrice);
}

Then, we need to implement these interfaces in our provider and observer classes. The provider class is responsible for fetching the current price of BTC from some external source (such as an API) and notifying the observers when the price changes. The observer classes are responsible for displaying or logging or processing the notifications from the provider. 

For example:


// A provider class that implements the provider interface
public class BtcPriceProvider : IBtcPriceProvider
{
    // A field that stores the current price
    private decimal currentPrice;

    // A field that stores a list of observers
    private List<IBtcPriceObserver> observers;

    // A constructor that initializes the fields
    public BtcPriceProvider()
    {
        currentPrice = 0;
        observers = new List<IBtcPriceObserver>();
    }

    // A method that implements the registration logic
    public void RegisterObserver(IBtcPriceObserver observer)
    {
        // Add the observer to the list if not already present
        if (!observers.Contains(observer))
        {
            observers.Add(observer);
        }
    }

    // A method that implements the notification logic
    public void NotifyObservers(decimal newPrice)
    {
        // Loop through each observer in the list
        foreach (var observer in observers)
        {
            // Call the observer's update method with the new price
            observer.Update(newPrice);
        }
    }

    // A method that fetches the current price from an API
    private decimal GetCurrentPrice()
    {
        // Some logic to call an API and get the current price
        // For simplicity, we use a random number here
        return new Random().Next(30000, 60000);
    }

    // A method that checks for price changes periodically
    public async Task CheckPriceAsync()
    {
        while (true)
        {
            // Get the current price from the API
            var newPrice = GetCurrentPrice();

            // Compare with the previous price
            if (newPrice != currentPrice)
            {
                // Update the current price
                currentPrice = newPrice;

                // Notify the observers with the new price
                NotifyObservers(newPrice);
            }

            // Wait for some time before checking again
            await Task.Delay(5000);
        }
    }
}

// An observer class that implements the observer interface and displays the price change on console
public class ConsoleDisplay : IBtcPriceObserver
{
    // A method that implements the update logic
    public void Update(decimal newPrice)
    {
        // Display the new price on console
        Console.WriteLine($"The new BTC price is {newPrice:C}");
    }
}

// An observer class that implements the observer interface and logs the price change to a file
public class FileLogger : IBtcPriceObserver
{
    // A method that implements the update logic
    public void Update(decimal newPrice)
    {
        // Log the new price to a file
        File.AppendAllText("log.txt", $"The new BTC price is {newPrice:C}\n");
    }
}

Finally, we need to use these interfaces to register and notify observers. We can create instances of our provider and observer classes and use their methods to interact with each other. 

For example:


// Create an instance of the provider class
var provider = new BtcPriceProvider();

// Create instances of the observer classes
var display = new ConsoleDisplay();
var logger = new FileLogger();

// Register the observers using their interface methods
provider.RegisterObserver(display);
provider.RegisterObserver(logger);

// Start checking for price changes asynchronously using their interface methods
await provider.CheckPriceAsync();

That’s it! We have implemented the observer pattern using custom interfaces in C#. 

Using custom interfaces for implementing the observer pattern is very flexible and powerful in C#, because interfaces allow us to define our own contracts and behaviors for providers and observers. However, custom interfaces also have some drawbacks, such as:

  • They require more code and effort to define and implement. We have to create our own interfaces and classes and write our own registration and notification logic.
  • They are not standardized or consistent. Different developers may use different names or signatures or conventions for their interfaces and classes, which may cause confusion or inconsistency.
  • They are not compatible or interoperable. Different implementations of custom interfaces may not work well with each other, unless they follow some common protocol or specification.

Using Built-in Interfaces

Using built-in interfaces means using existing interfaces that are defined by C# or .NET Framework for implementing providers and observers. This gives us more convenience and simplicity over how we implement and use the observer pattern.

To use built-in interfaces for implementing the observer pattern in C#, we can use two generic interfaces that are designed specifically for this purpose: IObservable<T> and IObserver<T>.

The IObservable<T> interface represents a provider that can be observed by one or more observers. It defines a single method: Subscribe, which allows an observer to subscribe to notifications from the provider.

The IObserver<T> interface represents an observer that can receive notifications from a provider. It defines three methods: OnNext, OnError, and OnCompleted, which are called by the provider when it has some data, an error, or no more data to send.

Using these interfaces for implementing the observer pattern in C#, we need to follow these steps:

  • Implement the IObservable<T> interface in our provider class. The T parameter represents the type of data that is sent by notifications.
  • Implement the IObserver<T> interface in our observer classes. The T parameter must match the provider’s parameter.
  • Use these interfaces to subscribe and notify observers.

Let’s see an example of how to use these interfaces for implementing the observer pattern.

We will use the same scenario as before: creating a simple app that monitors the price of Bitcoin (BTC) and notifies the users when the price changes. We will use the IObservable<T> and IObserver<T> interfaces to implement the observer pattern.

First, we need to implement the IObservable<T> interface in our provider class. The T parameter represents the type of data that is sent by notifications. In this case, we will use decimal as the type of data. 

For example:


// A provider class that implements the IObservable<decimal> interface
public class BtcPriceProvider : IObservable<decimal>
{
    // A field that stores the current price
    private decimal currentPrice;

    // A field that stores a list of observers
    private List<IObserver<decimal>> observers;

    // A constructor that initializes the fields
    public BtcPriceProvider()
    {
        currentPrice = 0;
        observers = new List<IObserver<decimal>>();
    }

    // A method that implements the subscription logic
    public IDisposable Subscribe(IObserver<decimal> observer)
    {
        // Add the observer to the list if not already present
        if (!observers.Contains(observer))
        {
            observers.Add(observer);
        }

        // Return an IDisposable object that allows the observer to unsubscribe
        return new Unsubscriber(observers, observer);
    }

    // A nested class that implements the IDisposable interface
    private class Unsubscriber : IDisposable
    {
        // A field that stores a reference to the list of observers
        private List<IObserver<decimal>> _observers;

        // A field that stores a reference to the observer to unsubscribe
        private IObserver<decimal> _observer;

        // A constructor that initializes the fields
        public Unsubscriber(List<IObserver<decimal>> observers, IObserver<decimal> observer)
        {
            _observers = observers;
            _observer = observer;
        }

        // A method that implements the unsubscription logic
        public void Dispose()
        {
            // Remove the observer from the list if still present
            if (_observer != null && _observers.Contains(_observer))
            {
                _observers.Remove(_observer);
            }
        }
    }

    // A method that fetches the current price from an API
    private decimal GetCurrentPrice()
    {
        // Some logic to call an API and get the current price
        // For simplicity, we use a random number here
        return new Random().Next(30000, 60000);
    }

    // A method that checks for price changes periodically and notifies observers
    public async Task CheckPriceAsync()
    {
        while (true)
        {
            // Get the current price from the API
            var newPrice = GetCurrentPrice();

            // Compare with the previous price
            if (newPrice != currentPrice)
            {
                // Update the current price
                currentPrice = newPrice;

                // Loop through each observer in the list
                foreach (var observer in observers)
                {
                    // Call the observer's OnNext method with the new price
                    observer.OnNext(newPrice);
                }
            }

            // Wait for some time before checking again
            await Task.Delay(5000);
        }
    }
}

Next, we need to implement the IObserver<T> interface in our observer classes. The T parameter must match the provider’s parameter. In this case, we will use decimal as well. 

For example:


// An observer class that implements the IObserver<decimal> interface and displays the price change on console
public class ConsoleDisplay : IObserver<decimal>
{
    // A field that stores a reference to an IDisposable object for unsubscription
    private IDisposable unsubscriber;

    // A method that implements the subscription logic
    public void Subscribe(IObservable<decimal> provider)
    {
        // Call the provider's Subscribe method and store the returned IDisposable object
        unsubscriber = provider.Subscribe(this);
    }

    // A method that implements the unsubscription logic
    public void Unsubscribe()
    {
        // Call the IDisposable object's Dispose method to unsubscribe from notifications
        unsubscriber.Dispose();
    }

    // A method that implements the notification logic for new data
    public void OnNext(decimal newPrice)
    {
        // Display the new price on console
        Console.WriteLine($"The new BTC price is {newPrice:C}");
    }

    // A method that implements the notification logic for errors (not used in this example)
    public void OnError(Exception error)
    {
        // Do nothing in this example
    }

    // A method that implements the notification logic for completion (not used in this example)
    public void OnCompleted()
    {
        // Do nothing in this example
    }
}

// An observer class that implements the IObserver<decimal> interface and logs the price change to a file
public class FileLogger : IObserver<decimal>
{
     // A field that stores a reference to an IDisposable object for unsubscription
     private IDisposable unsubscriber;

     // A method that implements the subscription logic
     public void Subscribe(IObservable<decimal> provider)
     {
         // Call the provider's Subscribe method and store the returned IDisposable object
         unsubscriber = provider.Subscribe(this);
     }

     // A method that implements the unsubscription logic
     public void Unsubscribe()
     {
         // Call the IDisposable object's Dispose method to unsubscribe from notifications
         unsubscriber.Dispose();
     }

     // A method that implements the notification logic for new data
     public void OnNext(decimal newPrice)
     {
         // Log the new price to a file
         File.AppendAllText("log.txt", $"The new BTC price is {newPrice:C}\n");
     }

     // A method that implements the notification logic for errors (not used in this example)
     public void OnError(Exception error)
     {
         // Do nothing in this example
     }

     // A method that implements the notification logic for completion (not used in this example)
     public void OnCompleted()
     {
         // Do nothing in this example
     }
}

Finally, we need to use these interfaces to subscribe and notify observers. We can create instances of our provider and observer classes and use their methods to interact with each other.

For example:


// Create an instance of the provider class
var provider = new BtcPriceProvider();

// Create instances of the observer classes
var display = new ConsoleDisplay();
var logger = new FileLogger();

// Subscribe to the provider using their interface methods
display.Subscribe(provider);
logger.Subscribe(provider);

// Start checking for price changes asynchronously using their interface methods
await provider.CheckPriceAsync();

// Unsubscribe from the provider using their interface methods (optional)
display.Unsubscribe();
logger.Unsubscribe();

That’s it! We have implemented the observer pattern using built-in interfaces in C#. 

Using built-in interfaces for implementing the observer pattern is very convenient and simple in C#, because interfaces are already defined and standardized by C# or .NET Framework and provide a clear and consistent contract for providers and observers. However, built-in interfaces also have some limitations, such as:

  • They require more boilerplate code and complexity to implement. We have to implement three methods for each observer and one method for each provider, as well as create a nested class for unsubscription logic.
  • They are not very expressive or descriptive. The interface methods have generic names and parameters that do not convey much information about the purpose or meaning of the notifications.
  • They are not very flexible or customizable. The interface methods have fixed signatures and behaviors that cannot be easily changed or extended to suit different scenarios or requirements.

Conclusion

In this article, we have learned what the observer pattern is and how to use it in C#. We have seen three ways of implementing the observer pattern: using events, using custom interfaces, and using built-in interfaces. Each way has its own advantages and disadvantages, and we should choose the one that best suits our needs and preferences.

The observer pattern is a very useful and common pattern in C#, especially in scenarios that involve push-based notifications. It allows us to create loosely coupled systems that can react to changes in other objects without depending on their classes or implementations.

I hope you enjoyed reading this article and learned something new. If you have any questions or feedback, please feel free to leave a comment below. And don’t forget to subscribe to my blog for more articles on C# and other technologies. 😊

Happy coding! 🚀

References



Code

You can find the code examples here: source code


Comments

Popular posts from this blog

Which GOF patterns are good for C#?

Angular on a regular SharePoint page (part 2)

Chain of Responsibility pattern in C#