ooad1-6 Exercise 3: Removing the Remaining Reach-Ins ======================================================= ex3/ex3.py (run from the chapter folder, where library_b.py, metrics.py and test_equivalence.py live; it writes library_b2.py): """Exercise 3: remove the reach-ins that version B still has, then check behaviour is unchanged and measure again. Run from the chapter folder (it needs library_b.py, metrics.py and test_equivalence.py beside it): py ex3/ex3.py """ import io import pathlib import sys import unittest HERE = pathlib.Path(__file__).resolve().parent.parent sys.path.insert(0, str(HERE)) import metrics # noqa: E402 source = (HERE / "library_b.py").read_text(encoding="utf8") def replace_once(text, old, new): assert text.count(old) == 1, old return text.replace(old, new) # Copy and Loan take over the changes to their own data (Information Expert), so Member stops writing into them. source = replace_once(source, ''' def check_can_be_lent(self) -> None: if self.on_loan: raise Refusal("already on loan") ''', ''' def check_can_be_lent(self) -> None: if self.on_loan: raise Refusal("already on loan") def lend(self) -> None: self.check_can_be_lent() self.on_loan = True def take_back(self) -> None: self.on_loan = False ''') source = replace_once(source, ''' def is_open(self) -> bool: return self.returned_on is None ''', ''' def is_open(self) -> bool: return self.returned_on is None def close(self, today: date) -> None: self.returned_on = today ''') source = replace_once(source, ''' copy.check_can_be_lent() loan = Loan(copy.barcode, today, today + timedelta(days=21)) copy.on_loan = True ''', ''' copy.lend() loan = Loan(copy.barcode, today, today + timedelta(days=21)) ''') source = replace_once(source, ''' loan.returned_on = today copy.on_loan = False return fine''', ''' loan.close(today) copy.take_back() return fine''') (HERE / "library_b2.py").write_text(source, encoding="utf8", newline="\n") metrics.show("library_b2.py (after the refinement)", str(HERE / "library_b2.py"), detail=True) # run the shared behaviour suite against the refined version by presenting it under the name the suite imports import library_b2 # noqa: E402 sys.modules["library_b"] = library_b2 import test_equivalence # noqa: E402 suite = unittest.defaultTestLoader.loadTestsFromModule(test_equivalence) result = unittest.TextTestRunner(stream=io.StringIO()).run(suite) print(f"behaviour suite on library_b2: {result.testsRun} tests, failures {len(result.failures) + len(result.errors)}") Run with: py ex3/ex3.py Output: library_b2.py (after the refinement) class methods named reach-in kind-tests Standard 2 0 0 0 Junior 2 0 0 0 Copy 3 0 0 0 Loan 4 0 0 0 Member 4 2 1 0 Catalogue 4 3 3 0 LoanDesk 3 2 0 0 reach-in in Member, line 99: copy.barcode reach-in in Catalogue, line 114: m.card reach-in in Catalogue, line 115: c.barcode reach-in in Catalogue, line 130: l.barcode TOTAL 22 4 0 behaviour suite on library_b2: 26 tests, failures 0 WHICH REACH-INS REMAIN, AND ARE THEY ACCEPTABLE? Catalogue: m.card, c.barcode, l.barcode. It reads identifying values to build and search its index. Acceptable: a lookup class has to know what it is looking up by. (Reading an identifier is far weaker than changing someone else's state.) Member: copy.barcode, read when creating the Loan. Acceptable for now: the loan needs to know which copy it is for. A tidier version would ask the copy to hand over a reference (copy.new_loan_data()), but that trades one small reach-in for more machinery. Gone: Member no longer writes copy.on_loan (Copy.lend and Copy.take_back do) and no longer sets loan.returned_on (Loan.close does). Those were the real lapses: one object changing another's fields. Total reach-ins: 21 in version A, 7 in B, 4 after this refinement. The 26-test behaviour suite passes on the refined version, so behaviour is unchanged. WHY THIS WORKS AS AN ANSWER --------------------------- Each state change now sits in the class that owns the state (Information Expert), and the rule 'a copy that is on loan cannot be lent again' is enforced inside Copy.lend rather than hoped for by whoever calls it. The refinement is checked twice: the shared suite for behaviour and the metric for structure. Neither alone would have been enough.