Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
370: The Observer Pattern
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
StockTickerclass (the Subject) that maintains a list of observers and a current price. - Create an abstract
StockObserverbase class with anupdate(self, price)method. - Implement a
PriceDisplayobserver that prints the current price every time it changes. - Implement a
BargainHunterobserver that takes athresholdprice 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.
There are no comments for now.