Behavioral Patterns II: Observer & State
Design Patterns
Chapter 8 · Behavioral Patterns II: Observer & State
Two more ways an object's behavior can vary — but this time neither one is about picking an algorithm. Observer lets a changing object automatically notify a whole list of interested parties, without knowing anything about who they are. State lets an object's own methods behave completely differently depending on what it currently is — with no if/elif chain checking that anywhere.
Observer: Automatic Notification on Change
A subject keeps a list of observers and notifies every one of them whenever something changes — each observer decides for itself what, if anything, to do with that notification.
logger and alert (threshold 150) both attached to the same Stock('ACME', 100): calling set_price(120) adds an entry to logger.log but leaves alert.alerts empty (120 doesn't exceed 150). Calling set_price(160) adds a second entry to logger.log and triggers alert.alerts for the first time. Stock never checked what kind of observer it was talking to — it just called .update() on everything in its list.
stock.detach(logger), calling set_price(200) leaves logger.log completely unchanged (still 2 entries) — but alert, which was never detached, correctly receives the update and grows to 2 entries of its own. One observer stopped listening; the other kept listening; Stock's own code needed no change to support either outcome.
State: Behavior That Changes With the Object's Own Condition
An order behaves differently depending on whether it's pending, paid, shipped, or delivered — calling .ship() should succeed on a paid order and fail on a pending one. State moves each condition's own rules into its own small class, and lets the main object simply delegate to whichever one is currently active.
OrderContext() (status 'Pending'), calling .ship() correctly raises 'Cannot ship an unpaid order', and the status is confirmed still 'Pending' afterward — the blocked call left no trace. Calling .pay() then .ship() then .deliver() in sequence moves the status through 'Paid' → 'Shipped' → 'Delivered' — each step calling a differently-behaving state object, even though OrderContext.ship()'s own code (self.state.ship(self)) never changed.
.pay() again on the now-'Delivered' order correctly raises 'Order already delivered', and the status is confirmed still 'Delivered' afterward — the exact same blocked-call-leaves-no-trace behavior verified earlier for the pending order, now demonstrated at the opposite end of the state sequence.
OrderContext.ship() would need its own if self.status == 'Paid': ... elif self.status == 'Pending': raise ... elif ... block — repeated inside pay(), ship(), and deliver() alike, with every new status requiring a new branch added to all three methods. Here, adding a new status means writing one new state class; none of OrderContext's three methods change at all.
Where This Connects
| This chapter's finding | What it connects to |
|---|---|
Observer's subject calling an identical .update() on every unknown-typed observer | The same "call the same method, let the object decide what happens" idea Chapter 7's Strategy already relied on — Observer applies it to notification instead of algorithm selection |
State's swappable self.state object, changed by the state objects themselves | Structurally close to Chapter 7's Strategy (composition, a swappable object) — but here the object being delegated to is the one deciding when to swap itself out, not an outside caller |
Both patterns eliminating a growing if/elif chain | Clean Code, SOLID & Refactoring's own treatment of conditional complexity as a recognized code smell |
Hands-On Exercises
Add a third observer, MovingAverageTracker, that records every price it receives and can report the average of all prices seen so far. Attach it alongside this chapter's own logger and alert, send three price updates, and verify its average is correct.
Add a CancelledState to this chapter's own order system, reachable via a new cancel() method that's only allowed from PendingState or PaidState (not after shipping). Verify cancelling a pending order succeeds, and cancelling a shipped order is correctly blocked.
Explain, using this chapter's own two verified patterns, why Stock needing to know nothing about PriceLogger or AlertService's own internals is the same underlying idea as OrderContext needing to know nothing about what PendingState.ship() versus PaidState.ship() actually do.
Chapter 8 Quick Reference
- Observer: a subject notifies a list of registered observers automatically on change, without knowing their concrete types — verified: two observers reacted differently to the same price change, and detaching one stopped only its own future updates while the other kept receiving them
- State: an object delegates its own method calls to a swappable "current state" object, so behavior changes with condition rather than through
if/elifchecks — verified: the identical.ship()call raised an exception fromPendingStatebut transitioned correctly fromPaidState, with a blocked call at both ends of the sequence leaving the status unchanged - Next chapter: Behavioral Patterns III — Command and Iterator