The Observer Design Pattern: Unlocking Efficient Notification Systems

Tobias Lang
9 min readMay 13, 2023

Introduction

In the world of software development, design patterns have become increasingly important as reusable solutions to common problems. These patterns provide a shared vocabulary and best practices that developers can utilize to build robust and maintainable software. One such design pattern is the Observer pattern, which is a part of the behavioral design pattern family¹. The Observer pattern establishes a one-to-many dependency between objects, allowing a single object to notify multiple dependents when its state changes. This pattern is particularly useful for promoting loose coupling between objects and enabling real-time updates.

This article aims to provide a comprehensive guide on the Observer design pattern, covering its structure, and use cases. By understanding the key concepts and practical applications of the Observer pattern, you can harness its full potential and create more scalable and flexible software systems.

The article is part of my series on Software Design Patterns. For a more general introduction to design patterns, refer to this overview article, which is an excellent starting point for exploring the fascinating world of software design patterns.

As usual, you can access the sample code featured in this article on my Github repository.

Two people juggling with clubs.
Photo by Los Muertos Crew: https://www.pexels.com/de-de/foto/mann-paar-frau-festhalten-8895422/

Observer Design Pattern

The Observer pattern is a design pattern that establishes a one-to-many dependency between objects so that when one object (called the Subject) changes its state, all its dependent objects (called Observers) are notified and updated automatically. It promotes loose coupling between the Subject and its Observers, making it easier to maintain and extend the system.

Observer Design Pattern (simplified UML)
  • Observable (Subject): The main object of interest in the system. It maintains a list of observers and provides methods to register, unregister, and notify these observers. When its state changes, it notifies all registered observers.
  • Observer: An interface that defines the update() method. This method is called by the Observable when its state changes. The Observer acts as a subscriber to the Observable's state changes.
  • ConcreteObservable (ConcreteSubject): A specific instance of the Observable class. It has a setState(state) method that changes its state and notifies all registered observers about this change. The ConcreteObservable encapsulates the core business logic that triggers state changes.
  • ConcreteObserver: A specific instance of the Observer interface. It implements the update() method to handle changes in the Observable's state. The ConcreteObserver represents the entities in the system that need to react to the Observable's state changes.

Implementation

Here is an example of how one might implement the Observer pattern in Python. The above diagram provides a practical insight into the application of the pattern:

from abc import ABC, abstractmethod
from typing import Optional


class Observer(ABC):
"""
The Observer interface declares the update method, used by subjects.
"""
@abstractmethod
def update(self, data: str) -> None:
pass

class ConcreteObserver(Observer):
"""
Concrete Observers react to the updates issued by the Subject they had been attached to.
"""
def __init__(self, name: str) -> None:
"""
Initialize a new observer.
"""
self._name = name
self.state: Optional[str] = None

def update(self, data: str) -> None:
"""
Receive update from subject.
"""
self.state = data
print(f"{self._name} received data: {data}")

class Observable:
"""
The Observable interface declares the methods of managing observers.
"""
def __init__(self) -> None:
"""
The list of observers. We can have multiple observers listening to.
"""
self._observers: List[Observer] = []

def register_observer(self, observer: Observer) -> None:
"""
Attach an observer to the subject.
"""
self._observers.append(observer)

def unregister_observer(self, observer: Observer) -> None:
"""
Remove an observer from the subject.
"""
self._observers.remove(observer)
def notify_observers(self, data: str) -> None:
"""
Notify all observers about an event.
"""
for observer in self._observers:
observer.update(data)

class ConcreteSubject(Observable):
"""
The Subject owns some important state and notifies observers
when the state changes.
"""

def __init__(self) -> None:
"""
Initialize a new subject.
"""
self._state: Optional[str] = None
super().__init__()

def set_state(self, data: str) -> None:
"""
Set the state of the subject and notify all observers.
"""
self._state = data
self.notify_observers(data)

# Usage
# Define objects
subject = ConcreteSubject()
observer1 = ConcreteObserver("Observer1")
observer2 = ConcreteObserver("Observer2")
# Register observers
subject.register_observer(observer1)
subject.register_observer(observer2)
# Update state, which notifies the observers
subject.set_state("Hello, Observers!")
# -> Observer1 received data: Hello, Observers!
# -> Observer2 received data: Hello, Observers!

The implementation of the Observer pattern is straightforward.

  • First define an abstract Observer class with an abstract method update(), which will be used to update all observers of state changes.
  • We then create an Observable class that maintains a list of observers and provides methods to register, unregister, and notify observers. The notify_observers() method loops through all registered observers and calls their update() method, passing in the new data.
  • Finally, we define the ConcreteObserver and ConcreteSubject classes, which implement the Observer and Observable interface respectively. The ConcreteObserver class overrides the update() method to print a message, while the ConcreteSubject class adds a set_state() method which is used to change its state and notify all observers.
  • In the client code, we create an instance of the ConcreteSubject and two instances of the ConcreteObserver. We register the two observers with the subject and then call set_state() on the subject, which triggers notifications to the observers.

Using Python’s built-in Observer and Observable

Now that we have seen a plain implementation of the Observer pattern, let’s have a look at how to improve readability. We already talked about Decorators (see https://medium.com/@tobiaslang/the-decorator-design-pattern-a-gateway-to-flexible-code-customization-f94cad1fbe1).

Python decorators can be used to simplify Observer registration, making it more concise and expressive:

from typing import Callable, Optional


class Observable:
"""
The Observable interface declares the methods of managing observers.
"""
def __init__(self) -> None:
"""
The list of observers. We can have multiple observers listening to.
"""
self._observers: List[Callable[[str], None]] = []

def register_observer(self, observer: Callable[[str], None]) -> None:
"""
Attach an observer to the subject.
"""
self._observers.append(observer)

def unregister_observer(self, observer: Callable[[str], None]) -> None:
"""
Remove an observer from the subject.
"""
self._observers.remove(observer)

def notify_observers(self, data: str) -> None:
"""
Notify all observers about an event.
"""
for observer in self._observers:
observer(data)

def observer(self, func: Callable[[str], None]):
"""
Decorator to register a function as an observer.
"""
self.register_observer(func)
return func

class ConcreteSubject(Observable):
"""
The Subject owns some important state and notifies observers when the state changes.
"""

def __init__(self) -> None:
"""
Initialize a new subject.
"""
self._state: Optional[str] = None
super().__init__()

def set_state(self, data: str) -> None:
"""
Set the state of the subject and notify all observers.
"""
self._state = data
self.notify_observers(data)

# Usage
# Define subject
subject = ConcreteSubject()

# Setup and register observers
@subject.observer
def observer1(data):
print(f"Observer1 received data: {data}")
@subject.observer
def observer2(data):
print(f"Observer2 received data: {data}")

# Update state, which notifies the observers
subject.set_state("Hello, Observers!")
# -> Observer1 received data: Hello, Observers!
# -> Observer2 received data: Hello, Observers!

We simplified the creation and registration of observers. With this implementation, there is no need for Observer and ConcreteObserver classes. Depending on the use case one wants to solve, developers can tailor the Observer pattern to their specific needs and preferences, making it a powerful tool in the Python ecosystem.

Advantages and Disadvantages

Let’s explore the benefits as well as the potential drawbacks of employing the Observer pattern.

Advantages

  • Loose coupling: The Observer pattern promotes loose coupling between the Subject and its Observers, making it easier to add, remove, or modify Observers without changing the Subject’s implementation.
  • Scalability: This allows for the straightforward addition of new Observers or removal of existing ones at any point in the application’s lifecycle. This capability not only enhances the scalability of your system as it grows and evolves but also provides remarkable flexibility, accommodating changes in requirements or design with minimal disruption to existing code.
  • Real-time updates: A cornerstone feature of the Observer pattern, guaranteeing that all Observers are immediately notified as soon as the state of the Subject undergoes a change. This ensures that all components of the system maintain a consistent view of the Subject’s state, facilitating synchronization and coherence throughout the application.

Disadvantages

  • Memory consumption: The Subject is tasked with maintaining a list of references to all Observers, which can increase memory usage, particularly in systems with a large number of Observers. This could lead to significant memory overhead and therefore requires careful consideration and management, especially in memory-constrained environments.
  • Performance impact: The process of notifying all Observers can become increasingly time-consuming as the number of Observers grows, especially when the update operation involved is complex. This could potentially lead to latency issues and slower response times in your application, particularly in scenarios where real-time performance is critical.
  • Unnecessary updates: The Observer pattern can potentially result in unnecessary updates, as Observers might be notified of changes in the Subject’s state that are irrelevant to their function, leading to wasted computational resources. This could inadvertently increase the load on the system, resulting in decreased performance and efficiency, especially in scenarios where resources are limited or the number of updates is high.

The Observer Pattern in Python Standard Library

The Observer pattern is widely used in various programming languages, including Python. The Python standard library itself contains multiple examples where the Observer pattern is used implicitly or explicitly.

Property decorator

The property decorator in Python can be seen as an example of the Observer pattern. It allows you to add "getter" and "setter" methods to your class attributes, effectively enabling you to control how these attributes are accessed or modified. When the attribute's value changes, the setter method acts as an observer, allowing you to execute custom code or validate the new value.

Event-driven programming with asyncio

Python’s asyncio library is designed for asynchronous I/O and concurrency, providing a framework for event-driven programming. In this context, the Observer pattern is naturally used for handling events, such as network connections or file system changes. When an event occurs, the event loop (Subject) notifies all registered event handlers (Observers), which in turn perform the appropriate actions.

The logging module

The Python logging module is another example of the Observer pattern in action. The logging system is designed around a hierarchy of loggers, which can be configured to emit log records to various destinations, such as files or the console. Each logger can have multiple handlers (Observers) that process the log records, format them, and write them to the desired output. When a log record is created, the logger (Subject) notifies all its handlers, ensuring that the log record is processed accordingly.

Conclusion

The Observer design pattern serves as an elegant and powerful tool in the software developer’s toolbox, enabling the establishment of a one-to-many dependency among objects. This pattern is renowned for its ability to promote loose coupling, a key principle in software design that emphasizes minimizing dependencies among separate modules, thereby enhancing the system’s flexibility and modularity.

One of the main advantages of the Observer pattern is that it facilitates easy system maintenance and extension. By delegating the responsibility of state management to the individual observers, it becomes easier to add, remove, or modify observers without affecting the observable object’s implementation. This segregation of duties not only streamlines code updates but also makes the codebase more comprehensible and manageable.

However, despite its numerous benefits, the Observer pattern isn’t without its potential pitfalls. Memory consumption can become a concern, particularly in systems with a large number of observers, as each observer would require its own memory space. Additionally, performance can be impacted as the number of observers grows, leading to a potential slowdown in the notification process.

Furthermore, it’s essential to be aware of the potential for unexpected behavior due to the sequence in which observers receive notifications. If observers have dependencies on each other, the system may behave unpredictably.

When used judiciously and with consideration for these potential drawbacks, the Observer pattern can profoundly enhance the design and structure of your software, leading to code that’s not just more maintainable and extensible, but also more robust and scalable. As with all design patterns, understanding when and how to use the Observer pattern is key to leveraging its full potential.

¹ Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides

--

--