ooad1-5 Exercise 3: An Association Class =============================================== certification.assocclass.puml (PlantUML 1.2026.8): @startuml scale 1.4 hide empty methods left to right direction class Mechanic { name } class BikeType { name } class Certification { certifiedOn } Mechanic "0..*" -- "0..*" BikeType : certified for (Mechanic, BikeType) .. Certification @enduml ex3.py (Python 3.14): import unittest from dataclasses import dataclass from datetime import date @dataclass(frozen=True) class Mechanic: name: str @dataclass(frozen=True) class BikeType: name: str @dataclass(frozen=True) class Certification: # the association class: a fact about the PAIR (Mechanic, BikeType) mechanic: Mechanic bike_type: BikeType certified_on: date class Register: def __init__(self): self._certs: dict[tuple[Mechanic, BikeType], Certification] = {} def certify(self, mechanic: Mechanic, bike_type: BikeType, on: date) -> Certification: key = (mechanic, bike_type) if key in self._certs: # one link per pair, as the diagram's association class implies raise ValueError(f"{mechanic.name} is already certified for {bike_type.name}") self._certs[key] = Certification(mechanic, bike_type, on) return self._certs[key] def certified(self, mechanic: Mechanic, bike_type: BikeType) -> bool: return (mechanic, bike_type) in self._certs def mechanics_for(self, bike_type: BikeType) -> list[str]: return sorted(m.name for (m, t) in self._certs if t == bike_type) ada, bo = Mechanic("Ada"), Mechanic("Bo") road, ebike = BikeType("road"), BikeType("e-bike") class AssociationClass(unittest.TestCase): def test_many_to_many_with_a_fact_on_the_link(self): r = Register() c = r.certify(ada, road, date(2026, 1, 5)) r.certify(ada, ebike, date(2026, 6, 1)) r.certify(bo, road, date(2026, 2, 1)) self.assertEqual(c.certified_on, date(2026, 1, 5)) self.assertEqual(r.mechanics_for(road), ["Ada", "Bo"]) self.assertTrue(r.certified(ada, ebike)) self.assertFalse(r.certified(bo, ebike)) def test_one_certification_per_pair(self): r = Register() r.certify(ada, road, date(2026, 1, 5)) with self.assertRaises(ValueError): r.certify(ada, road, date(2026, 9, 1)) def test_the_consequence_to_discuss_recertification(self): r = Register() r.certify(ada, road, date(2026, 1, 5)) # With an association class there is nowhere to keep the history of a renewed certification. # If renewals matter, Certification should be an ordinary class with its own identity (many per pair). self.assertEqual(len(r._certs), 1) if __name__ == "__main__": unittest.main(verbosity=2) Run with: py ex3.py Output: test_many_to_many_with_a_fact_on_the_link (__main__.AssociationClass.test_many_to_many_with_a_fact_on_the_link) ... ok test_one_certification_per_pair (__main__.AssociationClass.test_one_certification_per_pair) ... ok test_the_consequence_to_discuss_recertification (__main__.AssociationClass.test_the_consequence_to_discuss_recertification) ... ok ---------------------------------------------------------------------- Ran 3 tests in 0.000s OK WHEN IT IS THE WRONG CHOICE An association class allows exactly one instance per (Mechanic, BikeType) pair, as the second test shows. If certifications are renewed and the history matters (a mechanic certified for road bikes in 2024 and again in 2026), the pair would need many Certifications. Then Certification should be an ordinary class with an identity of its own, with an association to Mechanic and another to BikeType. The third test records this consequence rather than hiding it. WHY THIS WORKS AS AN ANSWER --------------------------- The dashed line from the middle of the association to Certification says the date belongs to the pairing, not to either end, and the dictionary keyed by the pair is that notation in code. Many-to-many works (Ada has two certifications, road has two mechanics) and the uniqueness rule is enforced where certifications are created.