ooad1-10 Exercise 1: Generate the Bike Shop, Then Finish It by Subclassing ================================================================================= shop.mmd (the design, Mermaid): classDiagram class JobStatus { <> OPEN DONE } class Pricing { <> +price(hours: float, part_pence: int) int } class StandardPricing { +hourly_pence: int +price(hours: float, part_pence: int) int } class Part { +part_no: str +price_pence: int } class JobPart { +quantity: int } class RepairJob { +hours: float +status: JobStatus +total(pricing: Pricing) int } class Bike { +description: str } StandardPricing ..|> Pricing RepairJob "1" *-- "0..*" JobPart JobPart "0..*" --> "1" Part RepairJob "0..*" --> "1" Bike Generate with: py uml2py.py shop.mmd shop_generated.py (uml2py.py is the chapter's generator) shop_generated.py (generated, never edited): """Generated from the class diagram by uml2py.py. Structure only: every operation is a stub.""" from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass, field from enum import Enum from typing import Protocol, runtime_checkable class JobStatus(Enum): OPEN = "OPEN" DONE = "DONE" @runtime_checkable class Pricing(Protocol): def price(self, hours: float, part_pence: int) -> int: ... @dataclass class StandardPricing: hourly_pence: int def price(self, hours: float, part_pence: int) -> int: raise NotImplementedError("StandardPricing.price") @dataclass class Part: part_no: str price_pence: int @dataclass class JobPart: quantity: int part: Part @dataclass class RepairJob: hours: float status: JobStatus bike: Bike job_parts: list[JobPart] = field(default_factory=list) def total(self, pricing: Pricing) -> int: raise NotImplementedError("RepairJob.total") @dataclass class Bike: description: str shop_impl.py (hand-written; subclasses the generated classes): """Hand-written behaviour, added by subclassing the generated classes (the 'generation gap' pattern), so regenerating never overwrites it.""" import unittest from dataclasses import dataclass import shop_generated as g @dataclass class StandardPricing(g.StandardPricing): def price(self, hours: float, part_pence: int) -> int: return int(hours * self.hourly_pence) + part_pence @dataclass class RepairJob(g.RepairJob): def total(self, pricing: g.Pricing) -> int: parts = sum(jp.part.price_pence * jp.quantity for jp in self.job_parts) return pricing.price(self.hours, parts) class Tests(unittest.TestCase): def job(self): job = RepairJob(1.5, g.JobStatus.OPEN, g.Bike("red road bike")) job.job_parts.append(g.JobPart(2, g.Part("TUBE", 450))) return job def test_generated_stub_alone_does_nothing_useful(self): with self.assertRaises(NotImplementedError): g.StandardPricing(3000).price(1.5, 900) def test_the_finished_classes_compute_a_total(self): self.assertEqual(self.job().total(StandardPricing(3000)), 5400) def test_the_implementation_still_fits_the_interface(self): self.assertIsInstance(StandardPricing(3000), g.Pricing) def test_a_required_link_cannot_be_omitted(self): with self.assertRaises(TypeError): RepairJob(1.0, g.JobStatus.OPEN) # the diagram says exactly one Bike if __name__ == "__main__": unittest.main() Run with: py shop_impl.py Output: ---------------------------------------------------------------------- Ran 4 tests in 0.000s OK WHY THIS WORKS AS AN ANSWER --------------------------- Regenerating shop_generated.py after a design change cannot erase the behaviour, because the behaviour lives in subclasses in a different file. The tests cover what each file contributes: the generated stub does nothing useful on its own (first test); the finished classes compute 5400p for 1.5 hours plus two 450p tubes (second); the subclass still fits the Pricing interface (third); and a link the diagram marks as exactly one (the Bike) is a required field, so omitting it fails (fourth). One thing the design did not say, found while writing the subclasses: the generated RepairJob names its list of parts job_parts, so the hand-written code has to use the generator's naming.