← Computer Programming II

Variant 1: Server CPU Monitor

Variant 1 — Server CPU Monitor

A monitoring system reports the server’s CPU usage every minute. Several teams subscribe to the readings, and each team has its own rule for when to act. Every action (and every reading) must end up in one shared log.

  1. Build a Singleton class LogBook. No matter how many times someone writes LogBook(), they must always get back the same object. The shared object holds a list called messages and a method write(text) that appends text to messages and prints >> {text}.
  2. Define an Enum called TeamKind with three members: SCALER, SAVER, AUDITOR.
  3. Create an abstract base class Policy with one abstract method act(cpu). act must return either the string "SCALE_UP", the string "SLEEP", or None.
  4. Create a concrete policy ScaleUp(limit) whose act(cpu) returns "SCALE_UP" when cpu > limit, otherwise None.
  5. Create a concrete policy Sleep(limit) whose act(cpu) returns "SLEEP" when cpu < limit, otherwise None.
  6. Create a concrete policy JustWatch whose act(cpu) always returns None.
  7. Create an abstract base class Subscriber with one abstract method update(cpu).
  8. Create a concrete class Team(Subscriber) with __init__(name, policy). Its update(cpu) asks the policy; if the result is None, it does nothing; otherwise it calls LogBook().write(f"{name} {action} (cpu={cpu})").
  9. Create a class Server with an empty _subs list, a method subscribe(sub) that appends to that list, and a method report(cpu) that first calls LogBook().write(f"cpu={cpu}%"), then calls update(cpu) on every subscriber in the order they subscribed.
  10. Define three builder functions, each takes one argument name and returns a fully configured Team:
    • make_scaler(name) → returns Team(name, ScaleUp(80))
    • make_saver(name) → returns Team(name, Sleep(20))
    • make_auditor(name) → returns Team(name, JustWatch())
  11. Create a class TeamFactory with a class variable _builders — a dict mapping each TeamKind to its builder function: SCALER → make_scaler, SAVER → make_saver, AUDITOR → make_auditor. Add a @staticmethod create(kind, name) that looks up the builder, raises ValueError(f"Unknown team: {kind}") if not found, and returns the result of calling that builder with name.

Usage

server = Server()
server.subscribe(TeamFactory.create(TeamKind.SCALER, "Alpha"))
server.subscribe(TeamFactory.create(TeamKind.SAVER, "Beta"))
server.subscribe(TeamFactory.create(TeamKind.AUDITOR, "Gamma"))

for cpu in [50, 90, 15, 70]:
    server.report(cpu)

print(f"Total messages: {len(LogBook().messages)}")

Expected Output

>> cpu=50%
>> cpu=90%
>> Alpha SCALE_UP (cpu=90)
>> cpu=15%
>> Beta SLEEP (cpu=15)
>> cpu=70%
Total messages: 6

Variant 2: Smart Greenhouse

Variant 2 — Smart Greenhouse

A greenhouse measures soil moisture (%) every hour. Several devices are attached to the greenhouse, and each device decides on its own whether to act on the latest reading. Every reading and every action must end up in one shared diary.

  1. Build a Singleton class Diary. No matter how many times someone writes Diary(), they must always get back the same object. The shared object holds a list called entries and a method note(text) that appends text to entries and prints [GH] {text}.
  2. Define an Enum called DeviceKind with three members: SPRINKLER, DRAINER, RECORDER.
  3. Create an abstract base class Action with one abstract method decide(moisture). decide must return either the string "WATER", the string "DRAIN", or None.
  4. Create a concrete action WaterIfDry(limit) whose decide(moisture) returns "WATER" when moisture < limit, otherwise None.
  5. Create a concrete action DrainIfWet(limit) whose decide(moisture) returns "DRAIN" when moisture > limit, otherwise None.
  6. Create a concrete action Idle whose decide(moisture) always returns None.
  7. Create an abstract base class Listener with one abstract method react(moisture).
  8. Create a concrete class Device(Listener) with __init__(name, action). Its react(moisture) asks the action; if the result is None, it does nothing; otherwise it calls Diary().note(f"{name} {result} (moisture={moisture})").
  9. Create a class Greenhouse with an empty _devices list, a method attach(device) that appends to that list, and a method measure(moisture) that first calls Diary().note(f"moisture={moisture}"), then calls react(moisture) on every device in the order they were attached.
  10. Define three builder functions, each takes one argument name and returns a fully configured Device:
    • make_sprinkler(name) → returns Device(name, WaterIfDry(30))
    • make_drainer(name) → returns Device(name, DrainIfWet(70))
    • make_recorder(name) → returns Device(name, Idle())
  11. Create a class DeviceFactory with a class variable _builders — a dict mapping each DeviceKind to its builder function: SPRINKLER → make_sprinkler, DRAINER → make_drainer, RECORDER → make_recorder. Add a @staticmethod create(kind, name) that looks up the builder, raises ValueError(f"Unknown device: {kind}") if not found, and returns the result of calling that builder with name.

Usage

gh = Greenhouse()
gh.attach(DeviceFactory.create(DeviceKind.SPRINKLER, "Pump"))
gh.attach(DeviceFactory.create(DeviceKind.DRAINER, "Valve"))
gh.attach(DeviceFactory.create(DeviceKind.RECORDER, "Notebook"))

for moisture in [50, 25, 80, 60]:
    gh.measure(moisture)

print(f"Total entries: {len(Diary().entries)}")

Expected Output

[GH] moisture=50
[GH] moisture=25
[GH] Pump WATER (moisture=25)
[GH] moisture=80
[GH] Valve DRAIN (moisture=80)
[GH] moisture=60
Total entries: 6

Variant 3: Hospital Vital Signs

Variant 3 — Hospital Vital Signs

A patient’s heart-rate monitor reports a new reading (bpm) every few seconds. Several staff members are assigned to the patient, and each one watches for a different problem. Every reading and every alert must end up in one shared chart.

  1. Build a Singleton class Chart. No matter how many times someone writes Chart(), they must always get back the same object. The shared object holds a list called notes and a method record(text) that appends text to notes and prints (+) {text}.
  2. Define an Enum called StaffKind with three members: NURSE, DOCTOR, OBSERVER.
  3. Create an abstract base class Protocol with one abstract method assess(bpm). assess must return either the string "BRADY", the string "TACHY", or None.
  4. Create a concrete protocol BradyAlert(limit) whose assess(bpm) returns "BRADY" when bpm < limit, otherwise None.
  5. Create a concrete protocol TachyAlert(limit) whose assess(bpm) returns "TACHY" when bpm > limit, otherwise None.
  6. Create a concrete protocol Quiet whose assess(bpm) always returns None.
  7. Create an abstract base class Watcher with one abstract method examine(bpm).
  8. Create a concrete class Staff(Watcher) with __init__(name, protocol). Its examine(bpm) asks the protocol; if the result is None, it does nothing; otherwise it calls Chart().record(f"{name} {result} (bpm={bpm})").
  9. Create a class Patient with an empty _watchers list, a method assign(watcher) that appends to that list, and a method vitals(bpm) that first calls Chart().record(f"bpm={bpm}"), then calls examine(bpm) on every watcher in the order they were assigned.
  10. Define three builder functions, each takes one argument name and returns a fully configured Staff:
    • make_nurse(name) → returns Staff(name, BradyAlert(50))
    • make_doctor(name) → returns Staff(name, TachyAlert(120))
    • make_observer(name) → returns Staff(name, Quiet())
  11. Create a class StaffFactory with a class variable _builders — a dict mapping each StaffKind to its builder function: NURSE → make_nurse, DOCTOR → make_doctor, OBSERVER → make_observer. Add a @staticmethod create(kind, name) that looks up the builder, raises ValueError(f"Unknown staff: {kind}") if not found, and returns the result of calling that builder with name.

Usage

patient = Patient()
patient.assign(StaffFactory.create(StaffKind.NURSE, "Harry"))
patient.assign(StaffFactory.create(StaffKind.DOCTOR, "Hermione"))
patient.assign(StaffFactory.create(StaffKind.OBSERVER, "Ron"))

for bpm in [75, 35, 90, 130]:
    patient.vitals(bpm)

print(f"Total notes: {len(Chart().notes)}")

Expected Output

(+) bpm=75
(+) bpm=35
(+) Harry BRADY (bpm=35)
(+) bpm=90
(+) bpm=130
(+) Hermione TACHY (bpm=130)
Total notes: 6

Variant 4: Bank Account Balance

Variant 4 — Bank Account Balance

A bank account broadcasts every change in its balance. Several watchers are bound to the account, and each one looks for a different problem. Every change and every alert must end up in one shared ledger.

  1. Build a Singleton class Ledger. No matter how many times someone writes Ledger(), they must always get back the same object. The shared object holds a list called entries and a method log(text) that appends text to entries and prints # {text}.
  2. Define an Enum called WatcherKind with three members: BORROWER, INVESTOR, AUDITOR.
  3. Create an abstract base class Rule with one abstract method check(balance). check must return either the string "BORROW", the string "INVEST", or None.
  4. Create a concrete rule LowBalance(limit) whose check(balance) returns "BORROW" when balance < limit, otherwise None.
  5. Create a concrete rule HighBalance(limit) whose check(balance) returns "INVEST" when balance > limit, otherwise None.
  6. Create a concrete rule NoOp whose check(balance) always returns None.
  7. Create an abstract base class Eye with one abstract method look(balance).
  8. Create a concrete class Watcher(Eye) with __init__(name, rule). Its look(balance) asks the rule; if the result is None, it does nothing; otherwise it calls Ledger().log(f"{name} {result} (balance={balance})").
  9. Create a class Account with an empty _eyes list, a method bind(eye) that appends to that list, and a method update(balance) that first calls Ledger().log(f"balance={balance}"), then calls look(balance) on every eye in the order they were bound.
  10. Define three builder functions, each takes one argument name and returns a fully configured Watcher:
    • make_borrower(name) → returns Watcher(name, LowBalance(1000))
    • make_investor(name) → returns Watcher(name, HighBalance(50000))
    • make_auditor(name) → returns Watcher(name, NoOp())
  11. Create a class WatcherFactory with a class variable _builders — a dict mapping each WatcherKind to its builder function: BORROWER → make_borrower, INVESTOR → make_investor, AUDITOR → make_auditor. Add a @staticmethod create(kind, name) that looks up the builder, raises ValueError(f"Unknown watcher: {kind}") if not found, and returns the result of calling that builder with name.

Usage

account = Account()
account.bind(WatcherFactory.create(WatcherKind.BORROWER, "Neo"))
account.bind(WatcherFactory.create(WatcherKind.INVESTOR, "Trinity"))
account.bind(WatcherFactory.create(WatcherKind.AUDITOR, "Morpheus"))

for balance in [3000, 500, 80000, 20000]:
    account.update(balance)

print(f"Total entries: {len(Ledger().entries)}")

Expected Output

# balance=3000
# balance=500
# Neo BORROW (balance=500)
# balance=80000
# Trinity INVEST (balance=80000)
# balance=20000
Total entries: 6

Variant 5: Warehouse Inventory

Variant 5 — Warehouse Inventory

A warehouse reports the stock count of an item every morning. Several officers are enrolled in the warehouse, and each one reacts to a different situation. Every report and every action must end up in one shared journal.

  1. Build a Singleton class Journal. No matter how many times someone writes Journal(), they must always get back the same object. The shared object holds a list called lines and a method write(text) that appends text to lines and prints * {text}.
  2. Define an Enum called RoleKind with three members: REORDERER, DISCOUNTER, TRACKER.
  3. Create an abstract base class Plan with one abstract method suggest(stock). suggest must return either the string "REORDER", the string "DISCOUNT", or None.
  4. Create a concrete plan Restock(limit) whose suggest(stock) returns "REORDER" when stock < limit, otherwise None.
  5. Create a concrete plan Promote(limit) whose suggest(stock) returns "DISCOUNT" when stock > limit, otherwise None.
  6. Create a concrete plan Silent whose suggest(stock) always returns None.
  7. Create an abstract base class Worker with one abstract method process(stock).
  8. Create a concrete class Officer(Worker) with __init__(name, plan). Its process(stock) asks the plan; if the result is None, it does nothing; otherwise it calls Journal().write(f"{name} {result} (stock={stock})").
  9. Create a class Warehouse with an empty _officers list, a method enroll(officer) that appends to that list, and a method report(stock) that first calls Journal().write(f"stock={stock}"), then calls process(stock) on every officer in the order they were enrolled.
  10. Define three builder functions, each takes one argument name and returns a fully configured Officer:
    • make_reorderer(name) → returns Officer(name, Restock(10))
    • make_discounter(name) → returns Officer(name, Promote(100))
    • make_tracker(name) → returns Officer(name, Silent())
  11. Create a class OfficerFactory with a class variable _builders — a dict mapping each RoleKind to its builder function: REORDERER → make_reorderer, DISCOUNTER → make_discounter, TRACKER → make_tracker. Add a @staticmethod create(kind, name) that looks up the builder, raises ValueError(f"Unknown role: {kind}") if not found, and returns the result of calling that builder with name.

Usage

wh = Warehouse()
wh.enroll(OfficerFactory.create(RoleKind.REORDERER, "Frodo"))
wh.enroll(OfficerFactory.create(RoleKind.DISCOUNTER, "Aragorn"))
wh.enroll(OfficerFactory.create(RoleKind.TRACKER, "Legolas"))

for stock in [50, 5, 150, 80]:
    wh.report(stock)

print(f"Total lines: {len(Journal().lines)}")

Expected Output

* stock=50
* stock=5
* Frodo REORDER (stock=5)
* stock=150
* Aragorn DISCOUNT (stock=150)
* stock=80
Total lines: 6

Variant 6: Dam Water Level

Variant 6 — Dam Water Level

A dam reports its water level (in meters) every hour. Several units are mounted on the dam, and each one reacts to a different situation. Every report and every action must end up in one shared logbook.

  1. Build a Singleton class Logbook. No matter how many times someone writes Logbook(), they must always get back the same object. The shared object holds a list called records and a method write(text) that appends text to records and prints ~ {text}.
  2. Define an Enum called UnitKind with three members: FLOODGATE, PUMP, CAMERA.
  3. Create an abstract base class Plan with one abstract method execute(level). execute must return either the string "OPEN", the string "PUMP_IN", or None.
  4. Create a concrete plan OpenIfFull(limit) whose execute(level) returns "OPEN" when level > limit, otherwise None.
  5. Create a concrete plan PumpIfLow(limit) whose execute(level) returns "PUMP_IN" when level < limit, otherwise None.
  6. Create a concrete plan Standby whose execute(level) always returns None.
  7. Create an abstract base class Sensor with one abstract method handle(level).
  8. Create a concrete class Unit(Sensor) with __init__(name, plan). Its handle(level) asks the plan; if the result is None, it does nothing; otherwise it calls Logbook().write(f"{name} {result} (level={level})").
  9. Create a class Dam with an empty _units list, a method mount(unit) that appends to that list, and a method read(level) that first calls Logbook().write(f"level={level}"), then calls handle(level) on every unit in the order they were mounted.
  10. Define three builder functions, each takes one argument name and returns a fully configured Unit:
    • make_floodgate(name) → returns Unit(name, OpenIfFull(80))
    • make_pump(name) → returns Unit(name, PumpIfLow(20))
    • make_camera(name) → returns Unit(name, Standby())
  11. Create a class UnitFactory with a class variable _builders — a dict mapping each UnitKind to its builder function: FLOODGATE → make_floodgate, PUMP → make_pump, CAMERA → make_camera. Add a @staticmethod create(kind, name) that looks up the builder, raises ValueError(f"Unknown unit: {kind}") if not found, and returns the result of calling that builder with name.

Usage

dam = Dam()
dam.mount(UnitFactory.create(UnitKind.FLOODGATE, "Gate-A"))
dam.mount(UnitFactory.create(UnitKind.PUMP, "Pump-B"))
dam.mount(UnitFactory.create(UnitKind.CAMERA, "Cam-C"))

for level in [50, 15, 90, 60]:
    dam.read(level)

print(f"Total records: {len(Logbook().records)}")

Expected Output

~ level=50
~ level=15
~ Pump-B PUMP_IN (level=15)
~ level=90
~ Gate-A OPEN (level=90)
~ level=60
Total records: 6

Variant 7: Power Grid Load

Variant 7 — Power Grid Load

A power grid reports its current load (in MW) every minute. Several plants are attached to the grid, and each one reacts to a different situation. Every report and every action must end up in one shared grid log.

  1. Build a Singleton class GridLog. No matter how many times someone writes GridLog(), they must always get back the same object. The shared object holds a list called entries and a method publish(text) that appends text to entries and prints @ {text}.
  2. Define an Enum called PlantKind with three members: GENERATOR, RESERVE, INSPECTOR.
  3. Create an abstract base class Policy with one abstract method react(load). react must return either the string "BOOST", the string "CUT", or None.
  4. Create a concrete policy BoostIfHeavy(limit) whose react(load) returns "BOOST" when load > limit, otherwise None.
  5. Create a concrete policy CutIfLight(limit) whose react(load) returns "CUT" when load < limit, otherwise None.
  6. Create a concrete policy Hold whose react(load) always returns None.
  7. Create an abstract base class Station with one abstract method cope(load).
  8. Create a concrete class Plant(Station) with __init__(name, policy). Its cope(load) asks the policy; if the result is None, it does nothing; otherwise it calls GridLog().publish(f"{name} {result} (load={load}MW)").
  9. Create a class Grid with an empty _plants list, a method attach(plant) that appends to that list, and a method report(load) that first calls GridLog().publish(f"load={load}MW"), then calls cope(load) on every plant in the order they were attached.
  10. Define three builder functions, each takes one argument name and returns a fully configured Plant:
    • make_generator(name) → returns Plant(name, BoostIfHeavy(80))
    • make_reserve(name) → returns Plant(name, CutIfLight(10))
    • make_inspector(name) → returns Plant(name, Hold())
  11. Create a class PlantFactory with a class variable _builders — a dict mapping each PlantKind to its builder function: GENERATOR → make_generator, RESERVE → make_reserve, INSPECTOR → make_inspector. Add a @staticmethod create(kind, name) that looks up the builder, raises ValueError(f"Unknown plant: {kind}") if not found, and returns the result of calling that builder with name.

Usage

grid = Grid()
grid.attach(PlantFactory.create(PlantKind.GENERATOR, "Hydro-1"))
grid.attach(PlantFactory.create(PlantKind.RESERVE, "Solar-2"))
grid.attach(PlantFactory.create(PlantKind.INSPECTOR, "Wind-3"))

for load in [40, 95, 5, 70]:
    grid.report(load)

print(f"Total entries: {len(GridLog().entries)}")

Expected Output

@ load=40MW
@ load=95MW
@ Hydro-1 BOOST (load=95MW)
@ load=5MW
@ Solar-2 CUT (load=5MW)
@ load=70MW
Total entries: 6

Variant 8: Race Car Engine

Variant 8 — Race Car Engine

A race car’s engine reports its current RPM every lap. Several crew members are enrolled with the engine, and each one reacts to a different situation. Every reading and every action must end up in one shared telemetry log.

  1. Build a Singleton class Telemetry. No matter how many times someone writes Telemetry(), they must always get back the same object. The shared object holds a list called frames and a method record(text) that appends text to frames and prints << {text}.
  2. Define an Enum called RoleKind with three members: DRIVER, MECHANIC, CAMERA.
  3. Create an abstract base class Strategy with one abstract method decide(rpm). decide must return either the string "SHIFT_UP", the string "BOOST", or None.
  4. Create a concrete strategy ShiftIfHigh(limit) whose decide(rpm) returns "SHIFT_UP" when rpm > limit, otherwise None.
  5. Create a concrete strategy BoostIfLow(limit) whose decide(rpm) returns "BOOST" when rpm < limit, otherwise None.
  6. Create a concrete strategy Watch whose decide(rpm) always returns None.
  7. Create an abstract base class Crew with one abstract method track(rpm).
  8. Create a concrete class Member(Crew) with __init__(name, strategy). Its track(rpm) asks the strategy; if the result is None, it does nothing; otherwise it calls Telemetry().record(f"{name} {result} (rpm={rpm})").
  9. Create a class Engine with an empty _members list, a method enroll(member) that appends to that list, and a method tick(rpm) that first calls Telemetry().record(f"rpm={rpm}"), then calls track(rpm) on every member in the order they were enrolled.
  10. Define three builder functions, each takes one argument name and returns a fully configured Member:
    • make_driver(name) → returns Member(name, ShiftIfHigh(7000))
    • make_mechanic(name) → returns Member(name, BoostIfLow(2000))
    • make_camera(name) → returns Member(name, Watch())
  11. Create a class MemberFactory with a class variable _builders — a dict mapping each RoleKind to its builder function: DRIVER → make_driver, MECHANIC → make_mechanic, CAMERA → make_camera. Add a @staticmethod create(kind, name) that looks up the builder, raises ValueError(f"Unknown role: {kind}") if not found, and returns the result of calling that builder with name.

Usage

engine = Engine()
engine.enroll(MemberFactory.create(RoleKind.DRIVER, "Luke"))
engine.enroll(MemberFactory.create(RoleKind.MECHANIC, "Leia"))
engine.enroll(MemberFactory.create(RoleKind.CAMERA, "Han"))

for rpm in [4000, 8000, 1500, 5000]:
    engine.tick(rpm)

print(f"Total frames: {len(Telemetry().frames)}")

Expected Output

<< rpm=4000
<< rpm=8000
<< Luke SHIFT_UP (rpm=8000)
<< rpm=1500
<< Leia BOOST (rpm=1500)
<< rpm=5000
Total frames: 6

Variant 9: Submarine Depth

Variant 9 — Submarine Depth

A submarine reports its current depth (in meters) every few seconds. Several modules are installed on the submarine, and each one reacts to a different situation. Every reading and every action must end up in one shared black box.

  1. Build a Singleton class BlackBox. No matter how many times someone writes BlackBox(), they must always get back the same object. The shared object holds a list called signals and a method store(text) that appends text to signals and prints >>> {text}.
  2. Define an Enum called SystemKind with three members: BALLAST, DIVE, SONAR.
  3. Create an abstract base class Procedure with one abstract method respond(depth). respond must return either the string "BLOW", the string "DIVE", or None.
  4. Create a concrete procedure BlowIfDeep(limit) whose respond(depth) returns "BLOW" when depth > limit, otherwise None.
  5. Create a concrete procedure DiveIfShallow(limit) whose respond(depth) returns "DIVE" when depth < limit, otherwise None.
  6. Create a concrete procedure Listen whose respond(depth) always returns None.
  7. Create an abstract base class Component with one abstract method signal(depth).
  8. Create a concrete class Module(Component) with __init__(name, procedure). Its signal(depth) asks the procedure; if the result is None, it does nothing; otherwise it calls BlackBox().store(f"{name} {result} (depth={depth}m)").
  9. Create a class Submarine with an empty _modules list, a method install(module) that appends to that list, and a method descend(depth) that first calls BlackBox().store(f"depth={depth}m"), then calls signal(depth) on every module in the order they were installed.
  10. Define three builder functions, each takes one argument name and returns a fully configured Module:
    • make_ballast(name) → returns Module(name, BlowIfDeep(300))
    • make_dive(name) → returns Module(name, DiveIfShallow(50))
    • make_sonar(name) → returns Module(name, Listen())
  11. Create a class ModuleFactory with a class variable _builders — a dict mapping each SystemKind to its builder function: BALLAST → make_ballast, DIVE → make_dive, SONAR → make_sonar. Add a @staticmethod create(kind, name) that looks up the builder, raises ValueError(f"Unknown system: {kind}") if not found, and returns the result of calling that builder with name.

Usage

sub = Submarine()
sub.install(ModuleFactory.create(SystemKind.BALLAST, "Tank-1"))
sub.install(ModuleFactory.create(SystemKind.DIVE, "Plane-2"))
sub.install(ModuleFactory.create(SystemKind.SONAR, "Ping-3"))

for depth in [100, 350, 30, 200]:
    sub.descend(depth)

print(f"Total signals: {len(BlackBox().signals)}")

Expected Output

>>> depth=100m
>>> depth=350m
>>> Tank-1 BLOW (depth=350m)
>>> depth=30m
>>> Plane-2 DIVE (depth=30m)
>>> depth=200m
Total signals: 6

Variant 10: Pizza Oven Temperature

Variant 10 — Pizza Oven Temperature

A pizza oven reports its current temperature (°C) every minute. Several gears are attached to the oven, and each one reacts to a different situation. Every reading and every action must end up in one shared sheet.

  1. Build a Singleton class Sheet. No matter how many times someone writes Sheet(), they must always get back the same object. The shared object holds a list called lines and a method mark(text) that appends text to lines and prints => {text}.
  2. Define an Enum called ToolKind with three members: BURNER, VENT, TIMER.
  3. Create an abstract base class Recipe with one abstract method cook(temp). cook must return either the string "FIRE", the string "COOL", or None.
  4. Create a concrete recipe FireIfCold(limit) whose cook(temp) returns "FIRE" when temp < limit, otherwise None.
  5. Create a concrete recipe CoolIfHot(limit) whose cook(temp) returns "COOL" when temp > limit, otherwise None.
  6. Create a concrete recipe Wait whose cook(temp) always returns None.
  7. Create an abstract base class Tool with one abstract method apply(temp).
  8. Create a concrete class Gear(Tool) with __init__(name, recipe). Its apply(temp) asks the recipe; if the result is None, it does nothing; otherwise it calls Sheet().mark(f"{name} {result} (temp={temp}C)").
  9. Create a class Oven with an empty _gears list, a method attach(gear) that appends to that list, and a method measure(temp) that first calls Sheet().mark(f"temp={temp}C"), then calls apply(temp) on every gear in the order they were attached.
  10. Define three builder functions, each takes one argument name and returns a fully configured Gear:
    • make_burner(name) → returns Gear(name, FireIfCold(200))
    • make_vent(name) → returns Gear(name, CoolIfHot(350))
    • make_timer(name) → returns Gear(name, Wait())
  11. Create a class GearFactory with a class variable _builders — a dict mapping each ToolKind to its builder function: BURNER → make_burner, VENT → make_vent, TIMER → make_timer. Add a @staticmethod create(kind, name) that looks up the builder, raises ValueError(f"Unknown tool: {kind}") if not found, and returns the result of calling that builder with name.

Usage

oven = Oven()
oven.attach(GearFactory.create(ToolKind.BURNER, "Flame"))
oven.attach(GearFactory.create(ToolKind.VENT, "Fan"))
oven.attach(GearFactory.create(ToolKind.TIMER, "Clock"))

for temp in [250, 150, 400, 300]:
    oven.measure(temp)

print(f"Total lines: {len(Sheet().lines)}")

Expected Output

=> temp=250C
=> temp=150C
=> Flame FIRE (temp=150C)
=> temp=400C
=> Fan COOL (temp=400C)
=> temp=300C
Total lines: 6

Variant 11: Air Quality Index

Variant 11 — Air Quality Index

A city’s air monitor reports the current Air Quality Index (AQI) every hour. Several services are attached to the monitor, and each one reacts to a different situation. Every reading and every action must end up in one shared bulletin.

  1. Build a Singleton class Bulletin. No matter how many times someone writes Bulletin(), they must always get back the same object. The shared object holds a list called posts and a method broadcast(text) that appends text to posts and prints [AQ] {text}.
  2. Define an Enum called ServiceKind with three members: HOSPITAL, PARK, CAMERA.
  3. Create an abstract base class Response with one abstract method act(aqi). act must return either the string "ALERT", the string "OPEN", or None.
  4. Create a concrete response AlertIfBad(limit) whose act(aqi) returns "ALERT" when aqi > limit, otherwise None.
  5. Create a concrete response OpenIfClean(limit) whose act(aqi) returns "OPEN" when aqi < limit, otherwise None.
  6. Create a concrete response Mute whose act(aqi) always returns None.
  7. Create an abstract base class Site with one abstract method react(aqi).
  8. Create a concrete class Service(Site) with __init__(name, response). Its react(aqi) asks the response; if the result is None, it does nothing; otherwise it calls Bulletin().broadcast(f"{name} {result} (aqi={aqi})").
  9. Create a class Atmosphere with an empty _services list, a method attach(service) that appends to that list, and a method report(aqi) that first calls Bulletin().broadcast(f"aqi={aqi}"), then calls react(aqi) on every service in the order they were attached.
  10. Define three builder functions, each takes one argument name and returns a fully configured Service:
    • make_hospital(name) → returns Service(name, AlertIfBad(150))
    • make_park(name) → returns Service(name, OpenIfClean(50))
    • make_camera(name) → returns Service(name, Mute())
  11. Create a class ServiceFactory with a class variable _builders — a dict mapping each ServiceKind to its builder function: HOSPITAL → make_hospital, PARK → make_park, CAMERA → make_camera. Add a @staticmethod create(kind, name) that looks up the builder, raises ValueError(f"Unknown service: {kind}") if not found, and returns the result of calling that builder with name.

Usage

air = Atmosphere()
air.attach(ServiceFactory.create(ServiceKind.HOSPITAL, "Clinic"))
air.attach(ServiceFactory.create(ServiceKind.PARK, "GreenZone"))
air.attach(ServiceFactory.create(ServiceKind.CAMERA, "EyeCam"))

for aqi in [80, 200, 30, 120]:
    air.report(aqi)

print(f"Total posts: {len(Bulletin().posts)}")

Expected Output

[AQ] aqi=80
[AQ] aqi=200
[AQ] Clinic ALERT (aqi=200)
[AQ] aqi=30
[AQ] GreenZone OPEN (aqi=30)
[AQ] aqi=120
Total posts: 6

Variant 12: Cargo Truck Weight

Variant 12 — Cargo Truck Weight

A weighing scale at a depot reports the truck’s current cargo weight (in kg) every check. Several workers are assigned to the scale, and each one reacts to a different situation. Every reading and every action must end up in one shared manifest.

  1. Build a Singleton class Manifest. No matter how many times someone writes Manifest(), they must always get back the same object. The shared object holds a list called items and a method enter(text) that appends text to items and prints ++ {text}.
  2. Define an Enum called WorkerKind with three members: LOADER, BRAKE, CAMERA.
  3. Create an abstract base class Routine with one abstract method decide(weight). decide must return either the string "LOAD_MORE", the string "HALT", or None.
  4. Create a concrete routine LoadIfLight(limit) whose decide(weight) returns "LOAD_MORE" when weight < limit, otherwise None.
  5. Create a concrete routine HaltIfHeavy(limit) whose decide(weight) returns "HALT" when weight > limit, otherwise None.
  6. Create a concrete routine Pause whose decide(weight) always returns None.
  7. Create an abstract base class Helper with one abstract method respond(weight).
  8. Create a concrete class Worker(Helper) with __init__(name, routine). Its respond(weight) asks the routine; if the result is None, it does nothing; otherwise it calls Manifest().enter(f"{name} {result} (weight={weight}kg)").
  9. Create a class Scale with an empty _workers list, a method assign(worker) that appends to that list, and a method weigh(weight) that first calls Manifest().enter(f"weight={weight}kg"), then calls respond(weight) on every worker in the order they were assigned.
  10. Define three builder functions, each takes one argument name and returns a fully configured Worker:
    • make_loader(name) → returns Worker(name, LoadIfLight(500))
    • make_brake(name) → returns Worker(name, HaltIfHeavy(2000))
    • make_camera(name) → returns Worker(name, Pause())
  11. Create a class WorkerFactory with a class variable _builders — a dict mapping each WorkerKind to its builder function: LOADER → make_loader, BRAKE → make_brake, CAMERA → make_camera. Add a @staticmethod create(kind, name) that looks up the builder, raises ValueError(f"Unknown worker: {kind}") if not found, and returns the result of calling that builder with name.

Usage

scale = Scale()
scale.assign(WorkerFactory.create(WorkerKind.LOADER, "Draco"))
scale.assign(WorkerFactory.create(WorkerKind.BRAKE, "Snape"))
scale.assign(WorkerFactory.create(WorkerKind.CAMERA, "Dumbledore"))

for weight in [800, 300, 2500, 1500]:
    scale.weigh(weight)

print(f"Total items: {len(Manifest().items)}")

Expected Output

++ weight=800kg
++ weight=300kg
++ Draco LOAD_MORE (weight=300kg)
++ weight=2500kg
++ Snape HALT (weight=2500kg)
++ weight=1500kg
Total items: 6