Skip to Content
Course content

370: The Observer Pattern

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this happen a dozen times in production code: a developer starts with a simple system where one event needs to trigger one action. It's easy. Then, the requirements grow. Suddenly, that one event needs to trigger five different actions across three different modules. If you aren't careful, you end up with a "God Class" that knows way too much about every other part of your system.

The Tight Coupling Trap

Take a look at this weather monitoring system. It seems fine at first glance, but there's a structural flaw that will make your life miserable as the project grows.

class PhoneDisplay:
    def update(self, temp):
        print(f"Phone Display: Temperature is now {temp}°C")

class FileLogger:
    def log(self, temp):
        print(f"Logging {temp}°C to a file...")

class WeatherStation:
    def __init__(self):
        self._temp = 0
        self.phone_display = PhoneDisplay()
        self.logger = FileLogger()

    def set_temperature(self, temp):
        self._temp = temp
        # Manually triggering every dependent system
        self.phone_display.update(self._temp)
        self.logger.log(self._temp)

station = WeatherStation()
station.set_temperature(25)

The problem here is tight coupling. The WeatherStation class is explicitly responsible for creating and calling the PhoneDisplay and the FileLogger. What happens when your boss tells you that we also need to send an email alert if the temperature exceeds 30°C? Or a push notification to a tablet? Or a signal to an industrial cooling system?

You'd have to go back into the WeatherStation class, add a new attribute to __init__, and add another line to set_temperature. You're modifying a core piece of logic just to add a peripheral feature. That's a recipe for regression bugs.

Decoupling with the Observer Pattern

The Observer Pattern solves this by turning the relationship around. Instead of the WeatherStation knowing exactly who needs the data, it just maintains a list of "observers" who have expressed interest. It doesn't care what those observers actually do with the data; it just notifies them that something changed.

Here is how we refactor this to be scalable.

from abc import ABC, abstractmethod

# We define a blueprint for any observer
class Observer(ABC):
    @abstractmethod
    def update(self, temperature):
        pass

class WeatherStation:
    def __init__(self):
        self._temp = 0
        self._observers = [] # A simple list of interested parties

    def attach(self, observer):
        if observer not in self._observers:
            self._observers.append(observer)

    def detach(self, observer):
        self._observers.remove(observer)

    def notify(self):
        for observer in self._observers:
            observer.update(self._temp)

    def set_temperature(self, temp):
        print(f"\nStation: New temperature recorded: {temp}°C")
        self._temp = temp
        self.notify()

# Now our specific tools just implement the Observer interface
class PhoneDisplay(Observer):
    def update(self, temp):
        print(f"Phone Display: Showing {temp}°C")

class FileLogger(Observer):
    def update(self, temp):
        print(f"File Logger: Writing {temp}°C to disk")

class AlertSystem(Observer):
    def update(self, temp):
        if temp > 30:
            print("Alert System: WARNING! High temperature detected!")

# Usage
station = WeatherStation()

phone = PhoneDisplay()
logger = FileLogger()
alerts = AlertSystem()

station.attach(phone)
station.attach(logger)
station.attach(alerts)

station.set_temperature(25) # Notifies all three
station.set_temperature(35) # Notifies all three, AlertSystem triggers warning

Notice the difference. I can now add a SatelliteUploader or a SmartThermostat class without ever touching the WeatherStation code again. The station is now "closed for modification but open for extension," which is a cornerstone of professional software design.

When to use this over a simple callback

You might be thinking, "Why not just pass a list of functions?" In very simple scripts, you can. But using a formal Observer pattern with a base class (like our Observer ABC) provides a strict contract. It ensures that every observer has an update method, which makes the code self-documenting and prevents AttributeError crashes when you're dealing with complex objects that have their own internal state.




📋 Practical Task

Build a Stock Market Price Tracker

You need to build a system that tracks the price of a specific stock and notifies different stakeholders when the price changes. Some stakeholders want a simple printout, while others only want to be notified if the price drops below a certain "buy" threshold.

Requirements:

  • Create a StockTicker class (the Subject) that maintains a list of observers and a current price.
  • Create an abstract StockObserver base class with an update(self, price) method.
  • Implement a PriceDisplay observer that prints the current price every time it changes.
  • Implement a BargainHunter observer that takes a threshold price in its constructor. It should only print a "BUY NOW!" message if the price drops below that threshold.
  • Instantiate the ticker, attach both observers, and simulate a few price changes (e.g., 150.00, 145.00, 130.00) to verify the behavior.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.