TUTORIAL SERIES

Design Patterns in Python: Observer

Publish and Subscribe

Amir Lavasani
7 min readJan 17, 2024

Have you encountered recurring coding challenges? Imagine having a toolbox of tried-and-true solutions readily available. That’s precisely what design patterns provide. In this series, we’ll explore what these patterns are and how they can elevate your coding skills.

Understanding the Observer Pattern

What is the Observer Design Pattern?

The Observer Design Pattern, classified as a behavioral design pattern, enables multiple objects to receive updates when changes occur in another object they are observing.

This approach resembles a notification system, informing various listeners about specific events.

When to Use the Observer Pattern:

The Observer Pattern proves effective in various scenarios:

  1. Event-Driven Architectures: Implement in systems requiring updates across multiple objects due to changes in one.
  2. Dynamic Object Interactions: When changes in one object require updates in other objects, especially when the exact set of objects isn’t predetermined or changes dynamically.
  3. Limited or Specific Observations: Use when certain objects need to observe others for a limited duration or under specific conditions.
  4. Dynamic Subscriptions: Leveraging a dynamic subscription list, enabling objects to join or leave the list as per their requirements.

Practical Example: Chat Application

To demonstrate the effectiveness of the Observer pattern and illustrate its functionality, we will implement a straightforward chat application using Python.

In this chat application, users will function as observers. Whenever a user sends a message, all other users within the chat, acting as observers, will receive and display the message.

Dall-E generated image with the following concept: Abstract eyes in diverse angles, embodying the vigilant observation of the Observer pattern, capturing entities keenly monitoring and adapting to changes

Terminology and Key Components

Understanding the fundamental components of the Observer pattern is crucial. Here are the key components involved:

  1. Publisher: Manages events triggered by changes in its state or behavior, incorporating a subscription infrastructure for subscriber management.
  2. Subscriber Interface: Declares the notification interface, typically including an update method to receive event details from the publisher.
  3. Concrete Subscribers: Implements specific actions in response to notifications issued by the publisher, ensuring adherence to a uniform interface for decoupling.
  4. Client: Creates instances of publishers and subscribers independently and orchestrates the subscription of subscribers to publishers for updates.
Observer design pattern structure diagram. Image from refactoring.guru

Observer Pattern Implementation in Python

Step 1: Subject

Manages a list of observers and notifies them of changes.

from abc import ABC, abstractmethod

# Step 1: The Subject
class Subject(ABC):
def __init__(self):
self._observers = set()

def attach(self, observer):
"""Adds an observer to the subject's list."""
self._observers.add(observer)

def detach(self, observer):
"""Removes an observer from the subject's list."""
self._observers.remove(observer)

def notify_observers(self):
"""Notifies all attached observers."""
for observer in self._observers:
observer.update()

Step 2: Observer Interface

Declares a method for receiving updates, ensuring conformity among observers.

# Step 2: The Observer Interface
class Observer(ABC):
@abstractmethod
def update(self):
"""Abstract method for receiving updates."""
pass

Step 3: Concrete Observer

Implements the update method to process received updates.

# Step 3: Concrete Observer
class ConcreteObserver(Observer):
def __init__(self, name):
self.name = name

def update(self):
"""Receives notification and prints it."""
print(f"{self.name} has been notified.")

Step 4: Client

Creates the subject, and observers, attaches observers to the subject, and notifies observers with a message.

# Step [4]: Client
if __name__ == "__main__":
# Create a subject
subject = Subject()

# Create observers
observer1 = ConcreteObserver("Observer1")
observer2 = ConcreteObserver("Observer2")
observer3 = ConcreteObserver("Observer3")

# Attach observers to the subject
subject.attach(observer1)
subject.attach(observer2)
subject.attach(observer3)

# Notify observers
subject.notify_observers()

GitHub Repo 🎉

Explore all code examples and design pattern implementations on GitHub!

Chat Application Implementation

In this section, we’ll implement a simple chatroom application using the Observer pattern.

Step 1: ChatRoom — Publisher

Manages the participants in the chat room and broadcasts messages to all participants.

from abc import ABC, abstractmethod

# Step 1: The ChatRoom - Publisher
class ChatRoom:
def __init__(self):
self._participants = set()

def join(self, participant):
"""Adds a new participant to the chat room."""
self._participants.add(participant)

def leave(self, participant):
"""Removes a participant from the chat room."""
self._participants.remove(participant)

def broadcast(self, message):
"""Sends a message to all participants in the chat room."""
for participant in self._participants:
participant.receive(message)

Step 2: Participant — Subscriber Interface

Defines the method for receiving messages, ensuring conformity among chat members.

# Step 2: Participant - Subscriber Interface
class Participant(ABC):
@abstractmethod
def receive(self, message):
"""Abstract method for receiving messages."""
pass

Step 3: ChatMember — Concrete Subscribers

Implements the receive method to display received messages.

# Step [3]: ChatMember - Concrete Subscribers
class ChatMember(Participant):
def __init__(self, name):
self.name = name

def receive(self, message):
"""Receives and displays the message."""
print(f"{self.name} received: {message}")

Step 4: Client

Creates the chat room, chat members, connects chat members to the chat room, and sends a welcome message to the chat room.

# Step [4]: Client
if __name__ == "__main__":
# Create a chat room
general_chat = ChatRoom()

# Create participants
user1 = ChatMember("User1")
user2 = ChatMember("User2")
user3 = ChatMember("User3")

# Participants join the chat room
general_chat.join(user1)
general_chat.join(user2)
general_chat.join(user3)

# Send a message to the chat room
general_chat.broadcast("Welcome to the chat!")
Dall-E generated image with the following concept: A minimalist watchtower silhouette amidst shifting backdrop, symbolizing observers as vigilant sentinels, constantly monitoring system changes.

10 Real-World Use Cases for Observer Pattern

  1. React.js: React utilizes the Observer pattern through its state and props mechanism, triggering updates in components upon state changes.
  2. JavaScript’s DOM Events: The DOM in browsers employs the Observer pattern to handle events triggered by user interactions.
  3. Java’s Event Handling: In Java, GUI frameworks like Swing and AWT utilize the Observer pattern for event handling.
  4. Node.js Event Emitter: Node.js’s core module ‘events’ is based on the Observer pattern, facilitating event-driven programming.
  5. Swift’s NotificationCenter: In iOS development, NotificationCenter implements the Observer pattern for broadcasting notifications across the app.
  6. Django Signals: In Python’s Django web framework, signals use the Observer pattern for handling specific actions during model events.
  7. Redux Library: Redux employs the Observer pattern to manage state changes and trigger updates in components.
  8. Amazon Web Services (AWS) SNS: AWS Simple Notification Service (SNS) uses the Observer pattern for push-based messaging and notifications.
  9. Kafka’s Pub/Sub Model: Apache Kafka employs the Observer pattern in its publish-subscribe model for message processing.
  10. Observer Pattern in EventBus in Android: The EventBus library facilitates communication between different parts of Android applications using the Observer pattern.

Best Practices and Considerations

In implementing the Observer pattern, it’s crucial to consider the following pros and cons:

Pros:

  1. Loose Coupling: Promotes loosely coupled objects, allowing easy changes without affecting others.
  2. Event-Driven Systems: Facilitates event-driven systems by separating event producers from consumers.
  3. Scalability: Enhances scalability by enabling multiple subscribers to respond to changes.

Cons:

  1. Complexity: This may introduce complexity in managing numerous observers and updates.
  2. Potential Overhead: Handling a high volume of notifications might impact performance.
  3. Synchronization: Ensuring thread safety when dealing with multiple concurrent notifications.

Best Practices:

  1. Clear Registration: Maintain clear registration methods for adding and removing observers.
  2. Observer Granularity: Define observer granularity, ensuring efficient and targeted notifications.

Considerations:

  1. Performance Impact: Assess potential performance impacts with a large number of observers.
  2. Resource Management: Manage resource consumption, especially in memory-intensive scenarios.
  3. Event Filtering: Consider implementing mechanisms to filter or prioritize events for optimization.
Dall-E generated image with the following concept: Minimalist Abstract radar wave scanning, illustrating observers’ continuous event detection

Observer Pattern’s Relations with Other Patterns

Understanding the relations between the Observer pattern and other design patterns illuminates their varying approaches to managing interactions:

Observer vs. Chain of Responsibility

Both patterns involve dynamic flows of information or requests.

Chain of Responsibility passes requests along a chain of potential receivers, while the Observer pattern facilitates subscription-based notifications.

Observer vs. Command

Both establish a form of unidirectional connection between senders and receivers.

Command encapsulates requests as objects, allowing parameterization and queuing of requests. In contrast, the Observer pattern focuses on subscription-based notifications without encapsulating requests.

Observer vs. Mediator

Both patterns promote indirect communication between components.

The Mediator eliminates direct connections between components, establishing a single mediator as the hub. Observer allows dynamic subscription and unsubscription of receivers, enabling more flexible relationships between objects.

Conclusion

In conclusion, the Observer pattern proves instrumental in establishing dynamic subscriptions and notifications between objects in software systems. Enabling flexible and loosely coupled communication, the Observer fosters scalable and responsive applications.

This pattern’s versatility in managing interactions among objects without direct dependencies significantly enhances maintainability and extensibility. Embracing the Observer pattern empowers developers to build adaptable systems capable of handling diverse interactions between components, contributing to robust and efficient software solutions.

Happy coding! 👩‍💻

Next on the Series 🚀

Read More 📜

The Series 🧭

References

  1. Design Patterns: Elements of Reusable Object-Oriented Software (Book)
  2. refactoring.guru Observer
  3. Head First Design Patterns (Book)
  4. Observer design pattern Microsoft Learn
  5. DigitalOcean Observer Design Pattern in Java
  6. Redis Pub/Sub

--

--

Amir Lavasani

I delve into machine learning 🤖 and software architecture 🏰 to enhance my expertise while sharing insights with Medium readers. 📃