💡 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. In designing a parking lot system using OOP, which component is best suited to decide which type of parking spot (e.g., compact, large, handicapped) should be allocated to an incoming vehicle?
easy
A. The ParkingLot class, as it manages all spots and vehicles
B. The ParkingSpot class, since it represents the spot's characteristics
C. The Vehicle class, because it knows its own size and type
D. A Factory or Strategy pattern component that encapsulates the allocation logic
Solution
Step 1: Understand responsibilities
The Vehicle class only knows about itself, not allocation rules. The ParkingSpot class represents a spot but doesn't decide allocation. The ParkingLot manages overall state but delegating allocation logic to a dedicated component improves modularity.
Step 2: Recognize design pattern role
The Factory or Strategy pattern encapsulates allocation logic, allowing easy extension and modification without changing core classes.
Final Answer:
Option D -> Option D
Quick Check:
Allocation logic centralized -> easier to maintain and extend [OK]
Hint: Allocation logic belongs in a dedicated pattern component, not core entities [OK]
Common Mistakes:
Assigning allocation responsibility to Vehicle or ParkingSpot classes
Putting all logic inside ParkingLot class leading to tight coupling
2. Consider the following Python code implementing the Composite pattern with iterators. Given a composite with two leaves named 'A' and 'B', what will be the output of iterating over the composite's iterator and collecting the names in order?
easy
A. ['B', 'A']
B. ['A', 'B']
C. ['root', 'A', 'B']
D. ['A']
Solution
Step 1: Trace the stack initialization
The CompositeIterator reverses children, so stack = [leafB, leafA].
Step 2: Trace iteration order
Pop leafA first (stack now [leafB]), then leafB (stack empty). Names collected in order: 'A', then 'B'.
Final Answer:
Option B -> Option B
Quick Check:
Reversing children in stack causes correct iteration order [OK]
Hint: Stack reversed children -> correct iteration order [OK]
Common Mistakes:
Assuming original order without reversing stack
Including composite node name in output
3. You need to design a system where a fixed sequence of steps is followed to prepare different types of beverages, but some steps vary depending on the beverage type. Which design approach best ensures code reuse and enforces the sequence while allowing subclasses to customize specific steps?
easy
A. Implement separate classes with duplicated code for each beverage type.
B. Apply a greedy algorithm to select steps dynamically at runtime based on beverage type.
C. Use a base class defining the skeleton of the algorithm with abstract methods for variable steps, overridden by subclasses.
D. Use a brute force approach that tries all possible step sequences and picks the correct one.
Solution
Step 1: Identify the problem structure
The problem requires a fixed sequence of steps with some steps varying by subclass.
Step 2: Match to design pattern
The Template Method Pattern defines a skeleton algorithm in a base class and lets subclasses override specific steps, ensuring reuse and consistent sequence.
Final Answer:
Option C -> Option C
Quick Check:
Base class controls sequence; subclasses customize steps [OK]
Hint: Fixed sequence with customizable steps -> Template Method [OK]
Common Mistakes:
Thinking duplicated code is acceptable for reuse
Confusing dynamic step selection with greedy algorithms
4. Considering extensibility in the Snake and Ladder game design, what is a key trade-off when embedding snakes and ladders directly as attributes inside the Board class versus modeling them as separate entities?
medium
A. Modeling snakes and ladders as separate entities increases runtime complexity significantly
B. Embedding snakes and ladders inside Board simplifies design but reduces flexibility to add new types of board elements later
C. Embedding snakes and ladders inside Board improves encapsulation and makes the Board immutable
D. Modeling snakes and ladders separately forces duplication of position data, increasing memory usage unnecessarily
Solution
Step 1: Embedding snakes/ladders inside Board
This approach simplifies initial design but tightly couples Board to these elements.
Step 2: Impact on extensibility
Tightly coupled design makes it harder to add new board elements (e.g., portals, traps) without modifying Board.
Step 3: Modeling as separate entities
Allows easy extension by adding new element types without changing Board internals.
Step 4: Complexity and memory considerations
Separate entities add minimal overhead; runtime complexity impact is negligible.
Final Answer:
Option B -> Option B
Quick Check:
Trade-off is between simplicity and extensibility, not runtime complexity or memory bloat.
Hint: Tight coupling simplifies now but blocks future extensions [OK]
Common Mistakes:
Thinking separate entities cause big runtime overhead
Believing embedding improves encapsulation and immutability
Assuming separate entities cause memory bloat
5. In a large-scale system where behaviors need to be dynamically changed at runtime, what is a key design consideration when using composition to avoid pitfalls that inheritance-based designs face?
hard
A. Use deep inheritance hierarchies to capture all behavior variations upfront.
B. Ensure that composed behavior objects are immutable to prevent side effects.
C. Avoid using interfaces and rely on concrete classes to reduce complexity.
D. Design behavior interfaces and use delegation so behaviors can be swapped without modifying the main object.
Solution
Step 1: Understand dynamic behavior change
To change behaviors at runtime, the design must support swapping behavior implementations easily.
Step 2: Analyze options
Ensure that composed behavior objects are immutable to prevent side effects. is not practical; immutability limits dynamic changes. Avoid using interfaces and rely on concrete classes to reduce complexity. increases coupling and reduces flexibility. Use deep inheritance hierarchies to capture all behavior variations upfront. contradicts favoring composition and leads to rigid designs.
Step 3: Confirm best practice
Design behavior interfaces and use delegation so behaviors can be swapped without modifying the main object. promotes defining behavior interfaces and delegating calls, enabling runtime swapping without modifying main objects.
Final Answer:
Option D -> Option D
Quick Check:
Delegation with interfaces is key to flexible, dynamic composition.
Hint: Use interfaces and delegation to swap behaviors dynamically, not inheritance or concrete classes.
Common Mistakes:
Thinking immutability aids dynamic behavior changes
Relying on inheritance for runtime flexibility
Recommended
Ready to prove it?
Pass the test and this pattern moves unknownstrongwhile earning +1 Grip toward your certificate.