The observer pattern tells many objects
The observer pattern lets one object tell many others when something changes, without knowing what any of them do. The state pattern lets an object change its behaviour when its state changes, without a pile of if tests. Both keep code easy to extend, and both appear in every program with events or a life cycle.
By the end of this lesson
Note
I can apply the observer and state patterns.
Key words
| Word | What it means |
|---|---|
| Observer | an object that wants to hear about changes |
| Subject | the object that tells its observers |
| Subscribe | to ask the subject to tell you |
| State pattern | one object for each state, which holds that state's behaviour |
| Transition | a move from one state to the next |
The cells students fill in are left blank in this preview.
A subject tells its observers when something changes
A temperature sensor takes a reading. A display wants to show it. An alarm wants to count readings that are too high. More listeners may come later.
If the sensor called each one by name, it would need editing for every new listener. In the observer pattern the sensor only keeps a list. Anything with an update method can subscribe.
A Sensor that tells its listeners
class Sensor:
def __init__(self):
self.listeners = []
def subscribe(self, listener):
self.listeners.append(listener)
def publish(self, value):
for listener in self.listeners:
listener.update(value)
class Display:
def __init__(self):
self.last = None
def update(self, value):
self.last = value
class Alarm:
def __init__(self, limit):
self.limit = limit
self.warnings = 0
def update(self, value):
if value > self.limit:
self.warnings = self.warnings + 1Make a folder for this lesson
mkdir -p ~/oop6Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Go into it
cd ~/oop6Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the classes to sensor.py
printf 'class Sensor:\n def __init__(self):\n self.listeners = []\n\n def subscribe(self, listener):\n self.listeners.append(listener)\n\n def publish(self, value):\n for listener in self.listeners:\n listener.update(value)\n\nclass Display:\n def __init__(self):\n self.last = None\n\n def update(self, value):\n self.last = value\n\nclass Alarm:\n def __init__(self, limit):\n self.limit = limit\n self.warnings = 0\n\n def update(self, value):\n if value > self.limit:\n self.warnings = self.warnings + 1\n' > sensor.pyShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Publish three readings to two listeners
python3 -c "from sensor import Sensor, Display, Alarm; s = Sensor(); d = Display(); a = Alarm(30); s.subscribe(d); s.subscribe(a); s.publish(25); s.publish(34); s.publish(31); print(d.last, a.warnings)"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Worked example: three readings
| Reading published | Display.last | Alarm.warnings (limit 30) |
|---|---|---|
| 25 | 25 | 0 |
| 34 | 34 | 1 |
| 31 | 31 | 2 |
The cells students fill in are left blank in this preview.
The state pattern gives each state a class
A job waits for a worker, then runs, then finishes. What it may do depends on its state. A waiting job can be cancelled. A running one cannot.
One way is an if and elif chain on a state word, in every method. With the state pattern, each state is a small class. The job holds the current one and asks it. The state also returns the next state, so each transition lives in one place.
Three state classes
class Waiting:
def describe(self):
return "waiting for a worker"
def can_cancel(self):
return True
def next_state(self):
return Running()
class Running:
def describe(self):
return "running"
def can_cancel(self):
return False
def next_state(self):
return Finished()
class Finished:
def describe(self):
return "finished"
def can_cancel(self):
return False
def next_state(self):
return selfA Job that holds one state
class Job:
def __init__(self):
self.state = Waiting()
def advance(self):
self.state = self.state.next_state()
def describe(self):
return self.state.describe()Write the classes to jobstate.py
printf 'class Waiting:\n def describe(self):\n return "waiting for a worker"\n def can_cancel(self):\n return True\n def next_state(self):\n return Running()\n\nclass Running:\n def describe(self):\n return "running"\n def can_cancel(self):\n return False\n def next_state(self):\n return Finished()\n\nclass Finished:\n def describe(self):\n return "finished"\n def can_cancel(self):\n return False\n def next_state(self):\n return self\n\nclass Job:\n def __init__(self):\n self.state = Waiting()\n def advance(self):\n self.state = self.state.next_state()\n def describe(self):\n return self.state.describe()\n' > jobstate.pyShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Move a job through its states
python3 -c "from jobstate import Job; j = Job(); print(j.describe()); j.advance(); print(j.describe()); j.advance(); print(j.describe()); j.advance(); print(j.describe())"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Ask the job when it can be cancelled
python3 -c "from jobstate import Job; j = Job(); print(j.state.can_cancel()); j.advance(); print(j.state.can_cancel())"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Strategy, observer and state side by side
| Pattern | The problem it solves | Who changes the object |
|---|---|---|
| Strategy | Several ways to do one job | The caller picks the strategy |
| Observer | Many objects need to hear about one change | Each listener subscribes itself |
| State | Behaviour depends on a life cycle | The states pick the next state |
The cells students fill in are left blank in this preview.
A common mistake
Warning
Many people think the subject must know what each listener does. It does not. It only knows that each has an update method. Keep it that way, so a new listener never needs a change to the subject.