Week 14 Assignment
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.
- Build a Singleton class
LogBook. No matter how many times someone writesLogBook(), they must always get back the same object. The shared object holds a list calledmessagesand a methodwrite(text)that appendstexttomessagesand prints>> {text}. - Define an
EnumcalledTeamKindwith three members:SCALER,SAVER,AUDITOR. - Create an abstract base class
Policywith one abstract methodact(cpu).actmust return either the string"SCALE_UP", the string"SLEEP", orNone. - Create a concrete policy
ScaleUp(limit)whoseact(cpu)returns"SCALE_UP"whencpu > limit, otherwiseNone. - Create a concrete policy
Sleep(limit)whoseact(cpu)returns"SLEEP"whencpu < limit, otherwiseNone. - Create a concrete policy
JustWatchwhoseact(cpu)always returnsNone. - Create an abstract base class
Subscriberwith one abstract methodupdate(cpu). - Create a concrete class
Team(Subscriber)with__init__(name, policy). Itsupdate(cpu)asks the policy; if the result isNone, it does nothing; otherwise it callsLogBook().write(f"{name} {action} (cpu={cpu})"). - Create a class
Serverwith an empty_subslist, a methodsubscribe(sub)that appends to that list, and a methodreport(cpu)that first callsLogBook().write(f"cpu={cpu}%"), then callsupdate(cpu)on every subscriber in the order they subscribed. - Define three builder functions, each takes one argument
nameand returns a fully configuredTeam:make_scaler(name)→ returnsTeam(name, ScaleUp(80))make_saver(name)→ returnsTeam(name, Sleep(20))make_auditor(name)→ returnsTeam(name, JustWatch())
- Create a class
TeamFactorywith a class variable_builders— a dict mapping eachTeamKindto its builder function:SCALER → make_scaler,SAVER → make_saver,AUDITOR → make_auditor. Add a@staticmethod create(kind, name)that looks up the builder, raisesValueError(f"Unknown team: {kind}")if not found, and returns the result of calling that builder withname.
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.
- Build a Singleton class
Diary. No matter how many times someone writesDiary(), they must always get back the same object. The shared object holds a list calledentriesand a methodnote(text)that appendstexttoentriesand prints[GH] {text}. - Define an
EnumcalledDeviceKindwith three members:SPRINKLER,DRAINER,RECORDER. - Create an abstract base class
Actionwith one abstract methoddecide(moisture).decidemust return either the string"WATER", the string"DRAIN", orNone. - Create a concrete action
WaterIfDry(limit)whosedecide(moisture)returns"WATER"whenmoisture < limit, otherwiseNone. - Create a concrete action
DrainIfWet(limit)whosedecide(moisture)returns"DRAIN"whenmoisture > limit, otherwiseNone. - Create a concrete action
Idlewhosedecide(moisture)always returnsNone. - Create an abstract base class
Listenerwith one abstract methodreact(moisture). - Create a concrete class
Device(Listener)with__init__(name, action). Itsreact(moisture)asks the action; if the result isNone, it does nothing; otherwise it callsDiary().note(f"{name} {result} (moisture={moisture})"). - Create a class
Greenhousewith an empty_deviceslist, a methodattach(device)that appends to that list, and a methodmeasure(moisture)that first callsDiary().note(f"moisture={moisture}"), then callsreact(moisture)on every device in the order they were attached. - Define three builder functions, each takes one argument
nameand returns a fully configuredDevice:make_sprinkler(name)→ returnsDevice(name, WaterIfDry(30))make_drainer(name)→ returnsDevice(name, DrainIfWet(70))make_recorder(name)→ returnsDevice(name, Idle())
- Create a class
DeviceFactorywith a class variable_builders— a dict mapping eachDeviceKindto its builder function:SPRINKLER → make_sprinkler,DRAINER → make_drainer,RECORDER → make_recorder. Add a@staticmethod create(kind, name)that looks up the builder, raisesValueError(f"Unknown device: {kind}")if not found, and returns the result of calling that builder withname.
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.
- Build a Singleton class
Chart. No matter how many times someone writesChart(), they must always get back the same object. The shared object holds a list callednotesand a methodrecord(text)that appendstexttonotesand prints(+) {text}. - Define an
EnumcalledStaffKindwith three members:NURSE,DOCTOR,OBSERVER. - Create an abstract base class
Protocolwith one abstract methodassess(bpm).assessmust return either the string"BRADY", the string"TACHY", orNone. - Create a concrete protocol
BradyAlert(limit)whoseassess(bpm)returns"BRADY"whenbpm < limit, otherwiseNone. - Create a concrete protocol
TachyAlert(limit)whoseassess(bpm)returns"TACHY"whenbpm > limit, otherwiseNone. - Create a concrete protocol
Quietwhoseassess(bpm)always returnsNone. - Create an abstract base class
Watcherwith one abstract methodexamine(bpm). - Create a concrete class
Staff(Watcher)with__init__(name, protocol). Itsexamine(bpm)asks the protocol; if the result isNone, it does nothing; otherwise it callsChart().record(f"{name} {result} (bpm={bpm})"). - Create a class
Patientwith an empty_watcherslist, a methodassign(watcher)that appends to that list, and a methodvitals(bpm)that first callsChart().record(f"bpm={bpm}"), then callsexamine(bpm)on every watcher in the order they were assigned. - Define three builder functions, each takes one argument
nameand returns a fully configuredStaff:make_nurse(name)→ returnsStaff(name, BradyAlert(50))make_doctor(name)→ returnsStaff(name, TachyAlert(120))make_observer(name)→ returnsStaff(name, Quiet())
- Create a class
StaffFactorywith a class variable_builders— a dict mapping eachStaffKindto its builder function:NURSE → make_nurse,DOCTOR → make_doctor,OBSERVER → make_observer. Add a@staticmethod create(kind, name)that looks up the builder, raisesValueError(f"Unknown staff: {kind}")if not found, and returns the result of calling that builder withname.
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.
- Build a Singleton class
Ledger. No matter how many times someone writesLedger(), they must always get back the same object. The shared object holds a list calledentriesand a methodlog(text)that appendstexttoentriesand prints# {text}. - Define an
EnumcalledWatcherKindwith three members:BORROWER,INVESTOR,AUDITOR. - Create an abstract base class
Rulewith one abstract methodcheck(balance).checkmust return either the string"BORROW", the string"INVEST", orNone. - Create a concrete rule
LowBalance(limit)whosecheck(balance)returns"BORROW"whenbalance < limit, otherwiseNone. - Create a concrete rule
HighBalance(limit)whosecheck(balance)returns"INVEST"whenbalance > limit, otherwiseNone. - Create a concrete rule
NoOpwhosecheck(balance)always returnsNone. - Create an abstract base class
Eyewith one abstract methodlook(balance). - Create a concrete class
Watcher(Eye)with__init__(name, rule). Itslook(balance)asks the rule; if the result isNone, it does nothing; otherwise it callsLedger().log(f"{name} {result} (balance={balance})"). - Create a class
Accountwith an empty_eyeslist, a methodbind(eye)that appends to that list, and a methodupdate(balance)that first callsLedger().log(f"balance={balance}"), then callslook(balance)on every eye in the order they were bound. - Define three builder functions, each takes one argument
nameand returns a fully configuredWatcher:make_borrower(name)→ returnsWatcher(name, LowBalance(1000))make_investor(name)→ returnsWatcher(name, HighBalance(50000))make_auditor(name)→ returnsWatcher(name, NoOp())
- Create a class
WatcherFactorywith a class variable_builders— a dict mapping eachWatcherKindto its builder function:BORROWER → make_borrower,INVESTOR → make_investor,AUDITOR → make_auditor. Add a@staticmethod create(kind, name)that looks up the builder, raisesValueError(f"Unknown watcher: {kind}")if not found, and returns the result of calling that builder withname.
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.
- Build a Singleton class
Journal. No matter how many times someone writesJournal(), they must always get back the same object. The shared object holds a list calledlinesand a methodwrite(text)that appendstexttolinesand prints* {text}. - Define an
EnumcalledRoleKindwith three members:REORDERER,DISCOUNTER,TRACKER. - Create an abstract base class
Planwith one abstract methodsuggest(stock).suggestmust return either the string"REORDER", the string"DISCOUNT", orNone. - Create a concrete plan
Restock(limit)whosesuggest(stock)returns"REORDER"whenstock < limit, otherwiseNone. - Create a concrete plan
Promote(limit)whosesuggest(stock)returns"DISCOUNT"whenstock > limit, otherwiseNone. - Create a concrete plan
Silentwhosesuggest(stock)always returnsNone. - Create an abstract base class
Workerwith one abstract methodprocess(stock). - Create a concrete class
Officer(Worker)with__init__(name, plan). Itsprocess(stock)asks the plan; if the result isNone, it does nothing; otherwise it callsJournal().write(f"{name} {result} (stock={stock})"). - Create a class
Warehousewith an empty_officerslist, a methodenroll(officer)that appends to that list, and a methodreport(stock)that first callsJournal().write(f"stock={stock}"), then callsprocess(stock)on every officer in the order they were enrolled. - Define three builder functions, each takes one argument
nameand returns a fully configuredOfficer:make_reorderer(name)→ returnsOfficer(name, Restock(10))make_discounter(name)→ returnsOfficer(name, Promote(100))make_tracker(name)→ returnsOfficer(name, Silent())
- Create a class
OfficerFactorywith a class variable_builders— a dict mapping eachRoleKindto its builder function:REORDERER → make_reorderer,DISCOUNTER → make_discounter,TRACKER → make_tracker. Add a@staticmethod create(kind, name)that looks up the builder, raisesValueError(f"Unknown role: {kind}")if not found, and returns the result of calling that builder withname.
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.
- Build a Singleton class
Logbook. No matter how many times someone writesLogbook(), they must always get back the same object. The shared object holds a list calledrecordsand a methodwrite(text)that appendstexttorecordsand prints~ {text}. - Define an
EnumcalledUnitKindwith three members:FLOODGATE,PUMP,CAMERA. - Create an abstract base class
Planwith one abstract methodexecute(level).executemust return either the string"OPEN", the string"PUMP_IN", orNone. - Create a concrete plan
OpenIfFull(limit)whoseexecute(level)returns"OPEN"whenlevel > limit, otherwiseNone. - Create a concrete plan
PumpIfLow(limit)whoseexecute(level)returns"PUMP_IN"whenlevel < limit, otherwiseNone. - Create a concrete plan
Standbywhoseexecute(level)always returnsNone. - Create an abstract base class
Sensorwith one abstract methodhandle(level). - Create a concrete class
Unit(Sensor)with__init__(name, plan). Itshandle(level)asks the plan; if the result isNone, it does nothing; otherwise it callsLogbook().write(f"{name} {result} (level={level})"). - Create a class
Damwith an empty_unitslist, a methodmount(unit)that appends to that list, and a methodread(level)that first callsLogbook().write(f"level={level}"), then callshandle(level)on every unit in the order they were mounted. - Define three builder functions, each takes one argument
nameand returns a fully configuredUnit:make_floodgate(name)→ returnsUnit(name, OpenIfFull(80))make_pump(name)→ returnsUnit(name, PumpIfLow(20))make_camera(name)→ returnsUnit(name, Standby())
- Create a class
UnitFactorywith a class variable_builders— a dict mapping eachUnitKindto its builder function:FLOODGATE → make_floodgate,PUMP → make_pump,CAMERA → make_camera. Add a@staticmethod create(kind, name)that looks up the builder, raisesValueError(f"Unknown unit: {kind}")if not found, and returns the result of calling that builder withname.
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.
- Build a Singleton class
GridLog. No matter how many times someone writesGridLog(), they must always get back the same object. The shared object holds a list calledentriesand a methodpublish(text)that appendstexttoentriesand prints@ {text}. - Define an
EnumcalledPlantKindwith three members:GENERATOR,RESERVE,INSPECTOR. - Create an abstract base class
Policywith one abstract methodreact(load).reactmust return either the string"BOOST", the string"CUT", orNone. - Create a concrete policy
BoostIfHeavy(limit)whosereact(load)returns"BOOST"whenload > limit, otherwiseNone. - Create a concrete policy
CutIfLight(limit)whosereact(load)returns"CUT"whenload < limit, otherwiseNone. - Create a concrete policy
Holdwhosereact(load)always returnsNone. - Create an abstract base class
Stationwith one abstract methodcope(load). - Create a concrete class
Plant(Station)with__init__(name, policy). Itscope(load)asks the policy; if the result isNone, it does nothing; otherwise it callsGridLog().publish(f"{name} {result} (load={load}MW)"). - Create a class
Gridwith an empty_plantslist, a methodattach(plant)that appends to that list, and a methodreport(load)that first callsGridLog().publish(f"load={load}MW"), then callscope(load)on every plant in the order they were attached. - Define three builder functions, each takes one argument
nameand returns a fully configuredPlant:make_generator(name)→ returnsPlant(name, BoostIfHeavy(80))make_reserve(name)→ returnsPlant(name, CutIfLight(10))make_inspector(name)→ returnsPlant(name, Hold())
- Create a class
PlantFactorywith a class variable_builders— a dict mapping eachPlantKindto its builder function:GENERATOR → make_generator,RESERVE → make_reserve,INSPECTOR → make_inspector. Add a@staticmethod create(kind, name)that looks up the builder, raisesValueError(f"Unknown plant: {kind}")if not found, and returns the result of calling that builder withname.
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.
- Build a Singleton class
Telemetry. No matter how many times someone writesTelemetry(), they must always get back the same object. The shared object holds a list calledframesand a methodrecord(text)that appendstexttoframesand prints<< {text}. - Define an
EnumcalledRoleKindwith three members:DRIVER,MECHANIC,CAMERA. - Create an abstract base class
Strategywith one abstract methoddecide(rpm).decidemust return either the string"SHIFT_UP", the string"BOOST", orNone. - Create a concrete strategy
ShiftIfHigh(limit)whosedecide(rpm)returns"SHIFT_UP"whenrpm > limit, otherwiseNone. - Create a concrete strategy
BoostIfLow(limit)whosedecide(rpm)returns"BOOST"whenrpm < limit, otherwiseNone. - Create a concrete strategy
Watchwhosedecide(rpm)always returnsNone. - Create an abstract base class
Crewwith one abstract methodtrack(rpm). - Create a concrete class
Member(Crew)with__init__(name, strategy). Itstrack(rpm)asks the strategy; if the result isNone, it does nothing; otherwise it callsTelemetry().record(f"{name} {result} (rpm={rpm})"). - Create a class
Enginewith an empty_memberslist, a methodenroll(member)that appends to that list, and a methodtick(rpm)that first callsTelemetry().record(f"rpm={rpm}"), then callstrack(rpm)on every member in the order they were enrolled. - Define three builder functions, each takes one argument
nameand returns a fully configuredMember:make_driver(name)→ returnsMember(name, ShiftIfHigh(7000))make_mechanic(name)→ returnsMember(name, BoostIfLow(2000))make_camera(name)→ returnsMember(name, Watch())
- Create a class
MemberFactorywith a class variable_builders— a dict mapping eachRoleKindto its builder function:DRIVER → make_driver,MECHANIC → make_mechanic,CAMERA → make_camera. Add a@staticmethod create(kind, name)that looks up the builder, raisesValueError(f"Unknown role: {kind}")if not found, and returns the result of calling that builder withname.
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.
- Build a Singleton class
BlackBox. No matter how many times someone writesBlackBox(), they must always get back the same object. The shared object holds a list calledsignalsand a methodstore(text)that appendstexttosignalsand prints>>> {text}. - Define an
EnumcalledSystemKindwith three members:BALLAST,DIVE,SONAR. - Create an abstract base class
Procedurewith one abstract methodrespond(depth).respondmust return either the string"BLOW", the string"DIVE", orNone. - Create a concrete procedure
BlowIfDeep(limit)whoserespond(depth)returns"BLOW"whendepth > limit, otherwiseNone. - Create a concrete procedure
DiveIfShallow(limit)whoserespond(depth)returns"DIVE"whendepth < limit, otherwiseNone. - Create a concrete procedure
Listenwhoserespond(depth)always returnsNone. - Create an abstract base class
Componentwith one abstract methodsignal(depth). - Create a concrete class
Module(Component)with__init__(name, procedure). Itssignal(depth)asks the procedure; if the result isNone, it does nothing; otherwise it callsBlackBox().store(f"{name} {result} (depth={depth}m)"). - Create a class
Submarinewith an empty_moduleslist, a methodinstall(module)that appends to that list, and a methoddescend(depth)that first callsBlackBox().store(f"depth={depth}m"), then callssignal(depth)on every module in the order they were installed. - Define three builder functions, each takes one argument
nameand returns a fully configuredModule:make_ballast(name)→ returnsModule(name, BlowIfDeep(300))make_dive(name)→ returnsModule(name, DiveIfShallow(50))make_sonar(name)→ returnsModule(name, Listen())
- Create a class
ModuleFactorywith a class variable_builders— a dict mapping eachSystemKindto its builder function:BALLAST → make_ballast,DIVE → make_dive,SONAR → make_sonar. Add a@staticmethod create(kind, name)that looks up the builder, raisesValueError(f"Unknown system: {kind}")if not found, and returns the result of calling that builder withname.
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.
- Build a Singleton class
Sheet. No matter how many times someone writesSheet(), they must always get back the same object. The shared object holds a list calledlinesand a methodmark(text)that appendstexttolinesand prints=> {text}. - Define an
EnumcalledToolKindwith three members:BURNER,VENT,TIMER. - Create an abstract base class
Recipewith one abstract methodcook(temp).cookmust return either the string"FIRE", the string"COOL", orNone. - Create a concrete recipe
FireIfCold(limit)whosecook(temp)returns"FIRE"whentemp < limit, otherwiseNone. - Create a concrete recipe
CoolIfHot(limit)whosecook(temp)returns"COOL"whentemp > limit, otherwiseNone. - Create a concrete recipe
Waitwhosecook(temp)always returnsNone. - Create an abstract base class
Toolwith one abstract methodapply(temp). - Create a concrete class
Gear(Tool)with__init__(name, recipe). Itsapply(temp)asks the recipe; if the result isNone, it does nothing; otherwise it callsSheet().mark(f"{name} {result} (temp={temp}C)"). - Create a class
Ovenwith an empty_gearslist, a methodattach(gear)that appends to that list, and a methodmeasure(temp)that first callsSheet().mark(f"temp={temp}C"), then callsapply(temp)on every gear in the order they were attached. - Define three builder functions, each takes one argument
nameand returns a fully configuredGear:make_burner(name)→ returnsGear(name, FireIfCold(200))make_vent(name)→ returnsGear(name, CoolIfHot(350))make_timer(name)→ returnsGear(name, Wait())
- Create a class
GearFactorywith a class variable_builders— a dict mapping eachToolKindto its builder function:BURNER → make_burner,VENT → make_vent,TIMER → make_timer. Add a@staticmethod create(kind, name)that looks up the builder, raisesValueError(f"Unknown tool: {kind}")if not found, and returns the result of calling that builder withname.
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.
- Build a Singleton class
Bulletin. No matter how many times someone writesBulletin(), they must always get back the same object. The shared object holds a list calledpostsand a methodbroadcast(text)that appendstexttopostsand prints[AQ] {text}. - Define an
EnumcalledServiceKindwith three members:HOSPITAL,PARK,CAMERA. - Create an abstract base class
Responsewith one abstract methodact(aqi).actmust return either the string"ALERT", the string"OPEN", orNone. - Create a concrete response
AlertIfBad(limit)whoseact(aqi)returns"ALERT"whenaqi > limit, otherwiseNone. - Create a concrete response
OpenIfClean(limit)whoseact(aqi)returns"OPEN"whenaqi < limit, otherwiseNone. - Create a concrete response
Mutewhoseact(aqi)always returnsNone. - Create an abstract base class
Sitewith one abstract methodreact(aqi). - Create a concrete class
Service(Site)with__init__(name, response). Itsreact(aqi)asks the response; if the result isNone, it does nothing; otherwise it callsBulletin().broadcast(f"{name} {result} (aqi={aqi})"). - Create a class
Atmospherewith an empty_serviceslist, a methodattach(service)that appends to that list, and a methodreport(aqi)that first callsBulletin().broadcast(f"aqi={aqi}"), then callsreact(aqi)on every service in the order they were attached. - Define three builder functions, each takes one argument
nameand returns a fully configuredService:make_hospital(name)→ returnsService(name, AlertIfBad(150))make_park(name)→ returnsService(name, OpenIfClean(50))make_camera(name)→ returnsService(name, Mute())
- Create a class
ServiceFactorywith a class variable_builders— a dict mapping eachServiceKindto its builder function:HOSPITAL → make_hospital,PARK → make_park,CAMERA → make_camera. Add a@staticmethod create(kind, name)that looks up the builder, raisesValueError(f"Unknown service: {kind}")if not found, and returns the result of calling that builder withname.
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.
- Build a Singleton class
Manifest. No matter how many times someone writesManifest(), they must always get back the same object. The shared object holds a list calleditemsand a methodenter(text)that appendstexttoitemsand prints++ {text}. - Define an
EnumcalledWorkerKindwith three members:LOADER,BRAKE,CAMERA. - Create an abstract base class
Routinewith one abstract methoddecide(weight).decidemust return either the string"LOAD_MORE", the string"HALT", orNone. - Create a concrete routine
LoadIfLight(limit)whosedecide(weight)returns"LOAD_MORE"whenweight < limit, otherwiseNone. - Create a concrete routine
HaltIfHeavy(limit)whosedecide(weight)returns"HALT"whenweight > limit, otherwiseNone. - Create a concrete routine
Pausewhosedecide(weight)always returnsNone. - Create an abstract base class
Helperwith one abstract methodrespond(weight). - Create a concrete class
Worker(Helper)with__init__(name, routine). Itsrespond(weight)asks the routine; if the result isNone, it does nothing; otherwise it callsManifest().enter(f"{name} {result} (weight={weight}kg)"). - Create a class
Scalewith an empty_workerslist, a methodassign(worker)that appends to that list, and a methodweigh(weight)that first callsManifest().enter(f"weight={weight}kg"), then callsrespond(weight)on every worker in the order they were assigned. - Define three builder functions, each takes one argument
nameand returns a fully configuredWorker:make_loader(name)→ returnsWorker(name, LoadIfLight(500))make_brake(name)→ returnsWorker(name, HaltIfHeavy(2000))make_camera(name)→ returnsWorker(name, Pause())
- Create a class
WorkerFactorywith a class variable_builders— a dict mapping eachWorkerKindto its builder function:LOADER → make_loader,BRAKE → make_brake,CAMERA → make_camera. Add a@staticmethod create(kind, name)that looks up the builder, raisesValueError(f"Unknown worker: {kind}")if not found, and returns the result of calling that builder withname.
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