💡 Subject starts with no observers and a lock to ensure thread safety during modifications.
setup
Create Observer A
ConcreteObserver instance named 'Observer A' is created, initializing its name field.
💡 Observers represent subscribers that will receive notifications; naming helps track them.
Line:def __init__(self, name):
self.name = name
💡 ConcreteObserver instances hold identity to distinguish notifications.
setup
Create Observer B
ConcreteObserver instance named 'Observer B' is created, initializing its name field.
💡 Multiple observers allow demonstration of multiple subscriptions and notifications.
Line:def __init__(self, name):
self.name = name
💡 Observer B is now ready to subscribe and receive notifications.
subscribe
Observer A subscribes to 'news' event
Subject's subscribe method is called with Observer A and event type 'news'. The method acquires the lock and adds Observer A to the observers set for 'news'.
💡 Subscription registers Observer A to receive notifications for 'news' events.
Line:def subscribe(self, observer, event_type):
with self.lock:
if event_type not in self.observers:
self.observers[event_type] = set()
self.observers[event_type].add(observer)
💡 Observers are stored in a thread-safe manner per event type, allowing concurrent safe modifications.
subscribe
Observer B subscribes to 'sports' event
Subject's subscribe method is called with Observer B and event type 'sports'. The method acquires the lock and adds Observer B to the observers set for 'sports'.
💡 Observer B registers for a different event type, showing multiple event subscriptions.
Line:def subscribe(self, observer, event_type):
with self.lock:
if event_type not in self.observers:
self.observers[event_type] = set()
self.observers[event_type].add(observer)
💡 The observers dictionary now holds multiple event types, each with their own subscriber sets.
notify
Notify observers of 'news' event with message 'News event 1'
Subject's notify method is called for event type 'news'. It locks and takes a snapshot list of observers subscribed to 'news' to avoid concurrent modification during iteration.
💡 Taking a snapshot ensures safe iteration even if observers subscribe or unsubscribe concurrently.
Line:def notify(self, event_type, message):
with self.lock:
observers_snapshot = list(self.observers.get(event_type, []))
💡 Snapshotting observers prevents errors and preserves notification order during iteration.
notify
Observer A receives notification for 'news' event
Subject calls Observer A's update method with event type 'news' and message 'News event 1'. Observer A processes and prints the notification.
💡 This is the core notification delivery to the observer.
Line:for observer in observers_snapshot:
observer.update(event_type, message)
💡 Observers react to events by executing their update method with event details.
unsubscribe
Observer A unsubscribes from 'news' event
Subject's unsubscribe method is called with Observer A and event type 'news'. The method acquires the lock, removes Observer A from the 'news' observers set, and deletes the event key if empty.
💡 Unsubscription removes observers safely, cleaning up empty event entries.
Line:def unsubscribe(self, observer, event_type):
with self.lock:
if event_type in self.observers and observer in self.observers[event_type]:
self.observers[event_type].remove(observer)
if not self.observers[event_type]:
del self.observers[event_type]
💡 Subject maintains clean observer lists, preventing stale entries.
notify
Notify observers of 'news' event with message 'News event 2'
Subject's notify method is called for event type 'news'. It locks and attempts to take a snapshot of observers for 'news', but finds none since Observer A unsubscribed and the key was deleted.
💡 Notification to an event with no subscribers results in no notifications sent.
Line:def notify(self, event_type, message):
with self.lock:
observers_snapshot = list(self.observers.get(event_type, []))
💡 Subject handles empty observer sets gracefully without errors.
notify
Notify observers of 'sports' event with message 'News event 1'
Subject's notify method is called for event type 'sports'. It locks and takes a snapshot of observers subscribed to 'sports', which includes Observer B. Then it calls Observer B's update method.
💡 Observer B receives notification for its subscribed event type, demonstrating selective notification.
Line:def notify(self, event_type, message):
with self.lock:
observers_snapshot = list(self.observers.get(event_type, []))
for observer in observers_snapshot:
observer.update(event_type, message)
💡 Notifications are dispatched only to observers subscribed to the specific event type.
import threading
class Observer:
def update(self, event_type, message):
pass
class Subject:
def __init__(self): # STEP 1
self.lock = threading.Lock()
self.observers = {}
def subscribe(self, observer, event_type): # STEP 4,5
with self.lock:
if event_type not in self.observers:
self.observers[event_type] = set()
self.observers[event_type].add(observer)
def unsubscribe(self, observer, event_type): # STEP 8
with self.lock:
if event_type in self.observers and observer in self.observers[event_type]:
self.observers[event_type].remove(observer)
if not self.observers[event_type]:
del self.observers[event_type]
def notify(self, event_type, message): # STEP 6,7,9,10
with self.lock:
observers_snapshot = list(self.observers.get(event_type, []))
for observer in observers_snapshot:
observer.update(event_type, message)
class ConcreteObserver(Observer):
def __init__(self, name): # STEP 2,3
self.name = name
def update(self, event_type, message): # STEP 7,10
print(f"{self.name} received {event_type}: {message}")
# Example usage
subject = Subject() # STEP 1
obs1 = ConcreteObserver("Observer A") # STEP 2
obs2 = ConcreteObserver("Observer B") # STEP 3
subject.subscribe(obs1, "news") # STEP 4
subject.subscribe(obs2, "sports") # STEP 5
subject.notify("news", "News event 1") # STEP 6,7
subject.unsubscribe(obs1, "news") # STEP 8
subject.notify("news", "News event 2") # STEP 9
subject.notify("sports", "News event 1") # STEP 10
📊
Observer Pattern - Event System, Publish-Subscribe - Watch the Algorithm Execute, Step by Step
Watching each step shows how observers are added, notified, and removed safely and concurrently, clarifying the pattern's dynamic behavior.
Step 1/10
·Active fill★Answer cell
Subject encapsulates observer management with thread safety.
Subject
−lock: Lock
−observers: Dict[str, Set[Observer]]
+__init__()
+subscribe()
+unsubscribe()
+1 more
«abstract»Observer
+update()
ConcreteObserver
−name: str
+__init__()
+update()
ConcreteObserver ▷ Observer (1:1)
ConcreteObserver implements Observer interface with identity.
Subject
−lock: Lock
−observers: Dict[str, Set[Observer]]
+__init__()
+subscribe()
+unsubscribe()
+1 more
«abstract»Observer
+update()
ConcreteObserver
−name: str
+__init__()
+update()
ConcreteObserver ▷ Observer (1:1)
Multiple ConcreteObserver instances represent different subscribers.
Subject
−lock: Lock
−observers: Dict[str, Set[Observer]]
+__init__()
+subscribe()
+unsubscribe()
+1 more
«abstract»Observer
+update()
ConcreteObserver
−name: str
+__init__()
+update()
ConcreteObserver ▷ Observer (1:1)
Subject manages observers per event type using thread-safe locking.
Subject
−lock: Lock
−observers: Dict[str, Set[Observer]]
+subscribe()
ConcreteObserver
−name: str
+update()
Subject supports multiple event types with separate observer sets.
Subject
−lock: Lock
−observers: Dict[str, Set[Observer]]
+subscribe()
ConcreteObserver
−name: str
+update()
Notify uses snapshot to avoid concurrent modification issues.
Subject
−lock: Lock
−observers: Dict[str, Set[Observer]]
+notify()
ConcreteObserver
−name: str
+update()
ConcreteObserver handles event notification in update method.
Subject
−lock: Lock
−observers: Dict[str, Set[Observer]]
+notify()
ConcreteObserver
−name: str
+update()
Unsubscribe safely removes observers and cleans empty event keys.
Subject
−lock: Lock
−observers: Dict[str, Set[Observer]]
+unsubscribe()
ConcreteObserver
−name: str
+update()
Notify handles missing event keys by returning empty observer lists.
Subject
−lock: Lock
−observers: Dict[str, Set[Observer]]
+notify()
ConcreteObserver
−name: str
+update()
Selective notification based on event type subscription.
Subject
−lock: Lock
−observers: Dict[str, Set[Observer]]
+notify()
ConcreteObserver
−name: str
+update()
Key Takeaways
✓ Thread-safe management of observers ensures safe concurrent subscription, unsubscription, and notification.
This is hard to see from code alone because concurrency issues are subtle and not obvious without watching lock usage and snapshotting.
✓ Observers are grouped by event type, enabling selective notification only to interested subscribers.
Visualizing the observers dictionary keyed by event type clarifies how notifications are routed.
✓ Taking a snapshot of observers before notification prevents errors from concurrent modifications during iteration.
This step reveals why direct iteration over live collections can cause runtime errors, a detail often missed in static code reading.
Practice
(1/5)
1. When a new feature is added by extending a class hierarchy following the Open/Closed Principle, what is the typical sequence of events when the system executes a method call on the new subclass instance?
easy
A. The base class method is always executed first, then the subclass method overrides it afterward
B. The system duplicates the base class code inside the subclass to avoid modifying the base
C. The subclass method is invoked directly due to polymorphism, without modifying base class code
D. Both base and subclass methods execute sequentially because the base class is modified to call the subclass
Solution
Step 1: Understand polymorphic dispatch
Method calls on subclass instances invoke the subclass's overridden method directly.
Step 2: Base class code remains unchanged
The base class is closed for modification; no changes are made to call subclass methods explicitly.
Step 3: Why other options are incorrect
The base class method is always executed first, then the subclass method overrides it afterward incorrectly suggests base method runs first then subclass; Both base and subclass methods execute sequentially because the base class is modified to call the subclass implies base class modification; The system duplicates the base class code inside the subclass to avoid modifying the base suggests code duplication, violating DRY and OCP.
Final Answer:
Option C -> Option C
Quick Check:
Polymorphism enables extension without modifying base code.
Believing base class must be modified to support extension
Thinking code duplication is a valid OCP strategy
2. Imagine a class responsible for both data persistence and data validation. When a change in validation rules occurs, trace the impact on the class and explain what happens step-by-step.
easy
A. Only the validation methods need modification; persistence remains unaffected, so SRP is maintained.
B. Changing validation rules forces modifying the entire class, risking unintended side effects on persistence logic.
C. Validation changes automatically propagate to persistence without code changes due to tight coupling.
D. Persistence logic will break because validation and persistence are tightly integrated and inseparable.
Solution
Step 1: Identify responsibilities
The class handles both validation and persistence, two distinct reasons to change.
Step 2: Trace change impact
Changing validation rules requires modifying validation code inside the class.
Step 3: Side effects
Because persistence logic shares the class, changes risk affecting persistence unintentionally, increasing maintenance risk.
Step 4: SRP violation
This coupling violates SRP, as one reason to change (validation) affects unrelated functionality (persistence).
Final Answer:
Option B -> Option B
Quick Check:
One reason to change should not force changes in unrelated code -> SRP violation.
Hint: One reason to change means one place to modify.
Common Mistakes:
Assuming changes affect only related methods without side effects.
Believing tight coupling is harmless if code is in one class.
Thinking validation and persistence are always linked.
3. Identify the bug in the following decorator implementation that causes incorrect behavior when stacking decorators:
medium
A. Line 5: Recursive call to self.cost() instead of super().cost() causes infinite recursion
B. Line 6: Incorrect string formatting in description
C. Line 2: Incorrect call to super().__init__
D. Line 4: Missing amount parameter default value
Solution
Step 1: Analyze cost() method
cost() calls self.cost() recursively, causing infinite recursion instead of delegating to wrapped object.
Step 2: Identify correct delegation
Should call super().cost() to delegate to wrapped coffee object's cost method.
Hint: Decorator methods must delegate via super(), not self [OK]
Common Mistakes:
Calling self.cost() instead of super().cost() in decorators
Forgetting to delegate calls properly
4. What is the time complexity of calling the prepare_recipe method in the Template Method Pattern implementation for a beverage, assuming each step runs in constant time?
medium
A. O(1), since the number of steps is fixed and each step runs in constant time
B. O(log n), due to the hook method optimizing optional steps
C. O(n^2), because each step may call other steps recursively
D. O(n), where n is the number of steps in the recipe
Solution
Step 1: Identify number of steps
The template method defines a fixed sequence of steps (boil_water, brew, pour_in_cup, add_condiments).
Step 2: Analyze step execution time
Each step runs in constant time; the hook method only conditionally calls add_condiments but does not affect asymptotic complexity.
Final Answer:
Option D -> Option D
Quick Check:
Fixed steps with constant time each -> O(1) total [OK]
Hint: Fixed step count -> constant time complexity [OK]
Common Mistakes:
Confusing n as input size
Assuming recursion adds complexity
5. In a legacy system, a class handles both business logic and logging. You want to refactor it following SRP, but the logging code is tightly intertwined with business logic. What is the best approach to refactor this while respecting SRP?
hard
A. Ignore SRP in this case because legacy code should not be refactored.
B. Leave logging inside the class because extracting it would break existing functionality and increase risk.
C. Merge business logic and logging into a utility class to centralize all cross-cutting concerns.
D. Extract logging into a separate class and replace logging calls with calls to this new class, even if it requires modifying many places.
Solution
Step 1: Identify responsibilities
Business logic and logging are separate reasons to change.
Step 2: Refactor strategy
Extract logging into its own class to isolate changes related to logging.
Step 3: Address tight coupling
Though intertwined, refactoring calls to the new logging class improves maintainability and respects SRP.
Step 4: Evaluate other options
Options B and D avoid refactoring, risking future issues; C increases coupling by merging unrelated concerns.
Final Answer:
Option D -> Option D
Quick Check:
Extract and isolate responsibilities even if it requires effort -> SRP compliance.
Hint: Separate concerns even if intertwined; refactor stepwise.