ExerciseDifficulty: Advanced2 min

Practice: A Smart Home Remote With Undo

Each button turns a light on or sets the heater, and one button undoes the last press.

A remote control is a natural Command, and this exercise asks you to write the commands and decide what each stores. The kitchen light starts off and the heater starts at 16 °C. The remote's buttons turn the light on, set the heater to a given temperature, and undo the last press, as many times as there are presses to undo.

  1. Write LightOn and SetHeater with execute and undo, and a Remote that keeps the history.
  2. What must each command store, and when must it store it?
  3. Press light on, heater to 21, heater to 23, then undo three times. What are the light and the heater after each step?
Show a solution
py
class Light:
    def __init__(self, room: str) -> None:
        self.room, self.on = room, False


class Heater:
    def __init__(self) -> None:
        self.celsius = 16


class LightOn:
    def __init__(self, light: Light) -> None:
        self.light = light
        self.was_on = False

    def execute(self) -> None:
        self.was_on = self.light.on
        self.light.on = True

    def undo(self) -> None:
        self.light.on = self.was_on


class SetHeater:
    def __init__(self, heater: Heater, celsius: int) -> None:
        self.heater, self.celsius = heater, celsius
        self.previous = 0

    def execute(self) -> None:
        self.previous = self.heater.celsius
        self.heater.celsius = self.celsius

    def undo(self) -> None:
        self.heater.celsius = self.previous


class Remote:
    def __init__(self) -> None:
        self.history = []

    def press(self, command) -> None:
        command.execute()
        self.history.append(command)

    def undo(self) -> None:
        if self.history:
            self.history.pop().undo()


kitchen, heater, remote = Light("kitchen"), Heater(), Remote()


def show(step: str) -> None:
    print(f"{step:22} light {'on' if kitchen.on else 'off'}, heater {heater.celsius} C")


show("start")
remote.press(LightOn(kitchen))
show("light on")
remote.press(SetHeater(heater, 21))
show("heater to 21")
remote.press(SetHeater(heater, 23))
show("heater to 23")
remote.undo()
show("undo")
remote.undo()
show("undo")
remote.undo()
show("undo")
OutputRan with python:3.12-slim (Python 3.12.13)
start                  light off, heater 16 C
light on               light on, heater 16 C
heater to 21           light on, heater 21 C
heater to 23           light on, heater 23 C
undo                   light on, heater 21 C
undo                   light on, heater 16 C
undo                   light off, heater 16 C
Each command records the old value in `execute`, before changing it.

SetHeater stores the temperature it replaced, and must read it in execute, not in its constructor. The heater can change between the moment a command is created and the moment it runs, for instance when a remote creates each button's command once, at setup, so a value read in the constructor would be stale. LightOn stores whether the light was already on, so undoing it on a light that was on leaves it on.

The three undos run in reverse: 23 back to 21, 21 back to 16, then the light off. The remote's undo checks for an empty history, so a fourth press does nothing instead of raising.