Easier to read with cream paper, a tint, more contrast or larger text? Choose your colours and text, once, for every page.

Free lesson

Observer and state patterns

After this lesson: I can apply the observer and state patterns.

60 min lesson7 commandsEveryday Linux commands lab: the thirty commands engineers type from memory

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

WordWhat it means
Observeran object that wants to hear about changes
Subjectthe object that tells its observers
Subscribeto ask the subject to tell you
State patternone object for each state, which holds that state's behaviour
Transitiona 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

Code: python
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 + 1

Make a folder for this lesson

Command
mkdir -p ~/oop6

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Go into it

Command
cd ~/oop6

Shown 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

Command
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.py

Shown 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

Command
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 publishedDisplay.lastAlarm.warnings (limit 30)
25250
34341
31312

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

Code: python
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 self

A Job that holds one state

Code: python
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

Command
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.py

Shown 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

Command
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

Command
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

PatternThe problem it solvesWho changes the object
StrategySeveral ways to do one jobThe caller picks the strategy
ObserverMany objects need to hear about one changeEach listener subscribes itself
StateBehaviour depends on a life cycleThe 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.

About this lab and its source: Everyday Linux commands lab: the thirty commands engineers type from memory

Teaching a class? Set this module for your students with a teacher guide, marked assignments and a lab for every student.