👀 Observer Pattern Tutorial
The Observer Pattern is a behavioral design 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.
🔍 When to Use It?
- When changes to one object require changing others, and you don’t know how many objects need to change
- When an object should be able to notify other objects without making assumptions about who these objects are
- When you need a publish-subscribe model where publishers send messages to subscribers
🧩 Scenario
Imagine a weather monitoring application:
- A weather station (Subject) collects data about temperature, humidity, and pressure
- Multiple displays (Observers) show different visualizations of this data
- When weather data changes, all displays should update automatically
🧱 Components of the Observer Pattern
Subject: The object that has a state and notifies observers when it changes
Observer: An interface defining the update method that subjects call
ConcreteSubject: Implements the Subject interface and maintains state
ConcreteObserver: Implements the Observer interface to respond to subject updates
classDiagram
direction LR
class Subject {
<<interface>>
+registerObserver(observer)
+removeObserver(observer)
+notifyObservers()
}
class Observer {
<<interface>>
+update(data)
}
class ConcreteSubject {
-observers: List
-state
+registerObserver(observer)
+removeObserver(observer)
+notifyObservers()
+setState(newState)
}
class ConcreteObserver {
-subject: Subject
+update(data)
}
Subject <|-- ConcreteSubject
Observer <|-- ConcreteObserver
ConcreteSubject --> Observer : notifies
ConcreteObserver --> ConcreteSubject : observes
💻 Implementation
Let’s implement our weather station example:
Observer Interface
from abc import ABC, abstractmethod
class Observer(ABC):
@abstractmethod
def update(self, temperature, humidity, pressure):
pass
Subject Interface
from abc import ABC, abstractmethod
class Subject(ABC):
@abstractmethod
def register_observer(self, observer):
pass
@abstractmethod
def remove_observer(self, observer):
pass
@abstractmethod
def notify_observers(self):
pass
Concrete Subject: WeatherData
class WeatherData(Subject):
def __init__(self):
self.observers = []
self.temperature = 0
self.humidity = 0
self.pressure = 0
def register_observer(self, observer):
if observer not in self.observers:
self.observers.append(observer)
def remove_observer(self, observer):
if observer in self.observers:
self.observers.remove(observer)
def notify_observers(self):
for observer in self.observers:
observer.update(self.temperature, self.humidity, self.pressure)
def set_measurements(self, temperature, humidity, pressure):
self.temperature = temperature
self.humidity = humidity
self.pressure = pressure
self.notify_observers()
Concrete Observers: Display Elements
class CurrentConditionsDisplay(Observer):
def __init__(self, weather_data):
self.temperature = 0
self.humidity = 0
weather_data.register_observer(self)
def update(self, temperature, humidity, pressure):
self.temperature = temperature
self.humidity = humidity
self.display()
def display(self):
print(f"Current conditions: {self.temperature}°C and {self.humidity}% humidity")
class StatisticsDisplay(Observer):
def __init__(self, weather_data):
self.temperatures = []
weather_data.register_observer(self)
def update(self, temperature, humidity, pressure):
self.temperatures.append(temperature)
self.display()
def display(self):
avg = sum(self.temperatures) / len(self.temperatures)
print(f"Avg/Max/Min temperature: {avg:.1f}/{max(self.temperatures)}/{min(self.temperatures)}")
Client Code
# Create the WeatherData subject
weather_data = WeatherData()
# Create observers
current_display = CurrentConditionsDisplay(weather_data)
statistics_display = StatisticsDisplay(weather_data)
# Simulate new weather measurements
weather_data.set_measurements(25, 65, 1013)
# Output:
# Current conditions: 25°C and 65% humidity
# Avg/Max/Min temperature: 25.0/25/25
weather_data.set_measurements(27, 70, 1012)
# Output:
# Current conditions: 27°C and 70% humidity
# Avg/Max/Min temperature: 26.0/27/25
🌟 Benefits of Using Observer Pattern
- Loose Coupling: Subjects and observers are loosely coupled - they can interact without detailed knowledge of each other
- Dynamic Relationships: Observers can be added or removed at runtime
- Broadcast Communication: One update in the subject can trigger updates in multiple observers
- Reusability: Both subjects and observers can be reused independently
🔑 OOP Principles Supported
1. Open/Closed Principle
- The pattern allows adding new observers without modifying the subject
- New types of observers can be created without changing existing code
2. Single Responsibility Principle
- The subject focuses on maintaining state and notifying observers
- Each observer handles its specific response to updates
3. Encapsulation
- Observers don’t need to know how the subject’s internal state is managed
- The subject doesn’t need to know what observers do with the data
4. Abstraction
- The pattern uses interfaces to define the contract between subjects and observers
- Implementation details are hidden behind these interfaces
🚨 Common Pitfalls
- Memory Leaks: If observers aren’t properly removed when no longer needed
- Update Overhead: Too many observers or frequent updates can cause performance issues
- Unexpected Updates: Cascading updates can occur if observers also act as subjects
- Consistency Issues: If multiple threads are updating the subject simultaneously
🔄 Real-World Applications
- Event handling systems in user interfaces
- Subscription services (newsletters, notifications)
- Model-View-Controller (MVC) architecture (Model as subject, Views as observers)
- Distributed systems where components need to stay synchronized