Course 1 ยท Chapter 10 ยท From UML to Code and Back
A diagram and the code it describes are two descriptions of one thing, and nothing keeps them in step. This chapter looks at the three movements between them: forward (from a diagram to code), reverse (from code to a diagram), and keeping them honest (noticing when they have drifted apart). Each is done for real with Python, and the results include some disappointments, which are the useful part.
What was run, and what was not
Run here: a generator I wrote (about 150 lines) that reads a Mermaid class diagram and writes a Python skeleton; tests of what that skeleton does; pyreverse from pylint 4.1.2 (with astroid 4.3.4, installed in a scratch virtual environment, not globally) run on the library from chapter 9 and on the generated code; a round-trip comparison; and a small drift check. Python 3.14, Mermaid 12.1.0 for rendering.
Not run: anything in Java (Java is not installed here), so the Java column below is from general knowledge and was not compiled, and any commercial model-driven tool. The history of model-driven generation and why it faded is also general knowledge, not research.
What Each Diagram Element Becomes
Chapter 5 showed each relationship as running Python. Here they are in one table, with the Java equivalent for readers coming from there. The Java column was not compiled or run; treat it as a guide, not a tested reference.
In the diagram
Python
Java (not run)
Class with attributes
@dataclass with typed fields (or a plain class with __init__)
A class with fields, or a record for an immutable value
Operation
A method; the type hints carry the signature
A method
Visibility + - # ~
Name conventions (_x, __x); not enforced
public private protected and package-private; enforced
Static member
@staticmethod, @classmethod, a class attribute
static
Abstract class / operation
ABC with @abstractmethod
abstract class, abstract method
Interface («interface»)
typing.Protocol (structural) or an ABC (nominal)
interface (nominal: a class must say implements)
Enumeration
enum.Enum
enum
Generalisation
Subclass: class Standard(Policy)
extends
Realisation
A class with the right methods (Protocol), or inheriting an ABC
implements
Association, multiplicity 1
A field holding the object; required in __init__
A field, set in the constructor
0..1
Type | None = None
A nullable field (or Optional)
0..* or 1..*
list[Type] with field(default_factory=list)
List<Type>
Composition
The whole creates the parts (add_copy); parts hold no independent reference
Same: the whole creates and owns; final list
Dependency
A parameter or local variable; no field
Same
Constraint (1..*, "at most one open")
Code you must write (a check in a method), plus a test
Same
For illustration only, and not compiled, the library's Member, MemberKind and Loan would read something like this in Java:
// NOT COMPILED OR RUN: Java is not installed on the machine this course was written on
public interface MemberKind { int loanLimit(); int finePerDay(); }
public final class Standard implements MemberKind {
public int loanLimit() { return 5; }
public int finePerDay() { return 10; }
}
public record Loan(String barcode, java.time.LocalDate borrowedOn, java.time.LocalDate dueOn) {}
public class Member {
private final String card;
private final MemberKind kind;
private final java.util.List<Loan> loans = new java.util.ArrayList<>(); // composition: Member owns its Loans
public Member(String card, MemberKind kind) { this.card = card; this.kind = kind; }
}
The main differences to notice are the ones the table points at: Java enforces visibility and requires interfaces to be declared with implements, while Python's Protocol lets the "provides" arrow of chapter 9 exist only in the design.
Forward: From a Diagram to a Skeleton
The input is a Mermaid class diagram (the notation of chapter 5):
classDiagram
class CopyStatus {
<<enumeration>>
AVAILABLE
ON_LOAN
WITHDRAWN
}
class FineCalculator {
<<interface>>
+fine_for(due_on: date, returned_on: date) int
}
class DailyRateFine {
+pence_per_day: int
+fine_for(due_on: date, returned_on: date) int
}
class Policy {
<<abstract>>
+limit()* int
}
class StandardPolicy
class JuniorPolicy
class Title {
+isbn: str
+name: str
}
class Copy {
+barcode: str
+status: CopyStatus
+lend() None
}
class Member {
+name: str
+card: str
+policy: Policy
+borrow(copy: Copy, today: date) Loan
}
class Loan {
+borrowed_on: date
+due_on: date
}
DailyRateFine ..|> FineCalculator
StandardPolicy --|> Policy
JuniorPolicy --|> Policy
Title "1" *-- "1..*" Copy
Member "1" --> "0..*" Loan
Loan "0..*" --> "1" Copy
A small generator reads it. Its source is the best documentation of what it does and what it cannot:
"""Generate a Python skeleton from a Mermaid class diagram (a small subset of the notation).
py uml2py.py design.mmd generated_library.py
Understood: <<enumeration>>, <<interface>>, <<abstract>>; attributes 'name: type'; operations 'name(a: t) ret' (a trailing * = abstract);
relationships: --|> (generalisation), ..|> (realisation), *-- (composition), --> (association) with multiplicities.
What it cannot do is the whole point of the chapter: it generates structure, and no behaviour.
"""
import re
import sys
from dataclasses import dataclass, field
REL = re.compile(r'^\s*(\w+)\s*(?:"([^"]+)"\s*)?(--\|>|\.\.\|>|\*--|-->)\s*(?:"([^"]+)"\s*)?(\w+)\s*$')
@dataclass
class Cls:
name: str
kind: str = "class" # class | enumeration | interface | abstract
attrs: list = field(default_factory=list) # (name, type)
ops: list = field(default_factory=list) # (name, params, ret, abstract)
values: list = field(default_factory=list)
base: str | None = None
realises: list = field(default_factory=list)
parts: list = field(default_factory=list) # (type, multiplicity) composed
refs: list = field(default_factory=list) # (type, multiplicity) associated
def parse(text: str) -> dict[str, Cls]:
classes: dict[str, Cls] = {}
current = None
for raw in text.splitlines():
line = raw.strip()
if not line or line == "classDiagram":
continue
m = re.match(r"class (\w+)\s*(\{)?$", line)
if m:
current = classes.setdefault(m.group(1), Cls(m.group(1)))
if not m.group(2):
current = None
continue
if line == "}":
current = None
continue
if current is not None:
stereo = re.match(r"<<(\w+)>>", line)
if stereo:
current.kind = stereo.group(1)
continue
op = re.match(r"\+(\w+)\(([^)]*)\)(\*)?\s*(\w[\w\[\], |]*)?$", line)
if op:
current.ops.append((op.group(1), op.group(2), op.group(4) or "None", bool(op.group(3))))
continue
attr = re.match(r"\+(\w+):\s*(\S+)$", line)
if attr:
current.attrs.append((attr.group(1), attr.group(2)))
continue
if current.kind == "enumeration" and re.match(r"^\w+$", line):
current.values.append(line)
continue
raise ValueError(f"cannot read a member line in {current.name}: {line!r}")
r = REL.match(line)
if r:
left, lm, arrow, rm, right = r.groups()
classes.setdefault(left, Cls(left))
classes.setdefault(right, Cls(right))
if arrow == "--|>":
classes[left].base = right
elif arrow == "..|>":
classes[left].realises.append(right)
elif arrow == "*--":
classes[left].parts.append((right, rm or "1"))
else:
classes[left].refs.append((right, rm or "1"))
continue
raise ValueError(f"cannot read line: {line!r}")
return classes
def link_name(target: str, mult: str) -> str:
"""The field name for a link: CamelCase to snake_case, plural for 'many' links (Copy -> copies, JobPart -> job_parts)."""
name = re.sub(r"(?<!^)(?=[A-Z])", "_", target).lower()
if mult in ("0..*", "1..*", "*"):
name = name[:-1] + "ies" if name.endswith("y") else name + "s"
return name
def field_type(target: str, mult: str) -> tuple[str, str]:
"""(annotation, default) for a link to `target` with this multiplicity."""
if mult in ("0..*", "1..*", "*"):
return f"list[{target}]", " = field(default_factory=list)"
if mult == "0..1":
return f"{target} | None", " = None"
return target, ""
def emit(classes: dict[str, Cls]) -> str:
order = [c for c in classes.values() if c.kind == "enumeration"] + [c for c in classes.values() if c.kind == "interface"]
placed = {c.name for c in order}
rest = [c for c in classes.values() if c.name not in placed]
while rest: # bases before subclasses
for c in list(rest):
if c.base is None or c.base in placed:
order.append(c)
placed.add(c.name)
rest.remove(c)
uses_date = any("date" in t for c in classes.values() for _, t in c.attrs) or any("date" in p or "date" in r for c in classes.values() for _, p, r, _ in c.ops)
out = ['"""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"]
if uses_date:
out.append("from datetime import date")
out += ["from enum import Enum", "from typing import Protocol, runtime_checkable", ""]
for c in order:
out += ["", ""]
if c.kind == "enumeration":
out.append(f"class {c.name}(Enum):")
out += [f' {v} = "{v}"' for v in c.values] or [" pass"]
elif c.kind == "interface":
out += ["@runtime_checkable", f"class {c.name}(Protocol):"]
out += [f" def {n}(self{', ' + p if p else ''}) -> {r}: ..." for n, p, r, _ in c.ops] or [" pass"]
else:
bases = [c.base] if c.base else (["ABC"] if c.kind == "abstract" else [])
if c.kind != "abstract":
out.append("@dataclass")
out.append(f"class {c.name}({', '.join(bases)}):" if bases else f"class {c.name}:")
body = []
required = [f" {n}: {t}" for n, t in c.attrs]
defaulted = []
for target, mult in c.parts + c.refs:
ann, default = field_type(target, mult)
name = link_name(target, mult)
(defaulted if default else required).append(f" {name}: {ann}{default}")
if c.kind != "abstract":
body += required + defaulted
own = {n for n, _, _, _ in c.ops}
stubs = list(c.ops)
for iface in c.realises: # a realised interface's operations must exist here too
stubs += [(n, p, r, a) for n, p, r, a in classes[iface].ops if n not in own]
for n, p, r, abstract in stubs:
if abstract or c.kind == "abstract" and abstract:
body += ["", " @abstractmethod", f" def {n}(self{', ' + p if p else ''}) -> {r}: ..."]
else:
body += ["", f" def {n}(self{', ' + p if p else ''}) -> {r}:", f' raise NotImplementedError("{c.name}.{n}")']
out += body or [" pass"]
return "\n".join(out) + "\n"
if __name__ == "__main__":
src, dst = sys.argv[1], sys.argv[2]
code = emit(parse(open(src, encoding="utf8").read()))
open(dst, "w", encoding="utf8", newline="\n").write(code)
print(f"wrote {dst}: {len(code.splitlines())} lines")
The skeleton it wrote:
"""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 datetime import date
from enum import Enum
from typing import Protocol, runtime_checkable
class CopyStatus(Enum):
AVAILABLE = "AVAILABLE"
ON_LOAN = "ON_LOAN"
WITHDRAWN = "WITHDRAWN"
@runtime_checkable
class FineCalculator(Protocol):
def fine_for(self, due_on: date, returned_on: date) -> int: ...
@dataclass
class DailyRateFine:
pence_per_day: int
def fine_for(self, due_on: date, returned_on: date) -> int:
raise NotImplementedError("DailyRateFine.fine_for")
class Policy(ABC):
@abstractmethod
def limit(self) -> int: ...
@dataclass
class StandardPolicy(Policy):
pass
@dataclass
class JuniorPolicy(Policy):
pass
@dataclass
class Title:
isbn: str
name: str
copies: list[Copy] = field(default_factory=list)
@dataclass
class Copy:
barcode: str
status: CopyStatus
def lend(self) -> None:
raise NotImplementedError("Copy.lend")
@dataclass
class Member:
name: str
card: str
policy: Policy
loans: list[Loan] = field(default_factory=list)
def borrow(self, copy: Copy, today: date) -> Loan:
raise NotImplementedError("Member.borrow")
@dataclass
class Loan:
borrowed_on: date
due_on: date
copy: Copy
What the Skeleton Keeps, and What It Lost
Nine tests ask the skeleton what the diagram promised. Six check promises the code keeps; three record what the diagram never said (note the second group's names):
"""What does the generated skeleton actually do? Each test states a claim the diagram makes, and checks the code keeps it."""
import unittest
from datetime import date
import generated_library as g
class WhatTheDiagramPromises(unittest.TestCase):
def test_enumeration(self):
self.assertEqual([s.name for s in g.CopyStatus], ["AVAILABLE", "ON_LOAN", "WITHDRAWN"])
with self.assertRaises(ValueError):
g.CopyStatus("LOST")
def test_abstract_class_cannot_be_created(self):
with self.assertRaises(TypeError):
g.Policy()
def test_composition_gives_the_whole_a_list_of_parts(self):
t = g.Title("111", "Dune")
t.copies.append(g.Copy("B1", g.CopyStatus.AVAILABLE))
self.assertEqual(len(t.copies), 1)
def test_association_with_exactly_one_is_a_required_field(self):
with self.assertRaises(TypeError):
g.Loan(date(2026, 10, 10), date(2026, 10, 31)) # the diagram says a Loan has exactly one Copy
def test_the_realising_class_has_the_interface_operation(self):
self.assertIsInstance(g.DailyRateFine(10), g.FineCalculator)
def test_operations_are_stubs(self):
with self.assertRaises(NotImplementedError):
g.DailyRateFine(10).fine_for(date(2026, 10, 10), date(2026, 10, 12))
class WhatTheDiagramDidNotSay(unittest.TestCase):
def test_the_concrete_policies_cannot_be_created_yet(self):
"""The diagram draws StandardPolicy under Policy but, as UML conventionally does, does not repeat limit() in the subclass box.
The generated StandardPolicy therefore inherits an abstract operation it never implements."""
with self.assertRaises(TypeError):
g.StandardPolicy()
def test_the_generation_gap_way_to_finish_it(self):
"""Do not edit the generated file (the next generation overwrites it): add behaviour in a hand-written subclass."""
class Standard(g.StandardPolicy):
def limit(self) -> int:
return 5
self.assertEqual(Standard().limit(), 5)
def test_nothing_enforces_the_rules_the_diagram_could_not_say(self):
"""Multiplicity 1..* (a Title has at least one Copy) is not enforced: a Title can be created with none."""
self.assertEqual(g.Title("111", "Dune").copies, [])
if __name__ == "__main__":
unittest.main(verbosity=2)
----------------------------------------------------------------------
Ran 9 tests in 0.000s
OK
Kept: the enumeration rejects an unknown value; the abstract class cannot be created; composition gave Title a list of Copy; a link of multiplicity exactly one is a required field (omitting it raises TypeError); the realising class has the interface's operation; the operations are stubs.
Not said, and therefore broken:StandardPolicy and JuniorPolicy cannot be created. The diagram draws them under Policy and, as UML conventionally does, does not repeat the inherited operation limit() in each subclass box, but the code needs each concrete class to implement it. A diagram can leave out what a compiler cannot.
Not expressible: multiplicity 1..* (a title has at least one copy) is not enforced; a Title is created with none. The generator cannot turn a constraint into behaviour.
Never Edit the Generated File
Generated code that you edit is overwritten the next time you generate, and generated code that you do not edit stays a stub. The standard way out is the generation gap: keep the generated classes untouched and add behaviour in hand-written subclasses in another file. One test above does exactly that (class Standard(g.StandardPolicy) with limit()). Exercise 1 does it for a whole design.
What the generator cannot do at all is the point: it produced structure, and none of the behaviour (the fines, the limits, the rules). Behaviour is chapters 7 and 8's diagrams plus your head. Forward generation is a head start on boilerplate, not on design.
Reverse: From Code to a Diagram
pyreverse, part of pylint, reads Python source and writes class and package diagrams (I used its Mermaid output; its help lists other output formats, which I did not try). I ran it on the library from chapter 9 (pyreverse -o mmd -p library library) and rendered the Mermaid it wrote:
Mermaid rendered from pyreverse's output on the real code; I changed nothing. It is wide because it is one row of eleven boxes with only two connected groups.
Compared with the diagram I drew by hand for the same design, this is what it shows well:
Every class and every method with its types, read from the code (fine(today: date, kind: MemberKind) int): more complete and more exact than my hand drawing.
Inheritance (Junior, Standard to MemberKind), and a hollow-diamond link where one class receives another in its constructor (LoanDesk holds a Catalogue).
And what it leaves out or gets wrong. I checked each against the code:
What the code has
What pyreverse drew
Member.loans: list[Loan], a member owns its loans
The text loans : list[Loan] in the box, no arrow to Loan: a collection of objects is not drawn as an association
InMemoryCatalogue and JsonCatalogue provide Catalogue
No realisation arrows. They match the Protocol by shape and never name it, so nothing in the source says "implements". Catalogue appears as a plain class, not an interface
InMemoryCatalogue.save() is an empty method that does nothing, on purpose
Drawn as save()*: marked abstract, which it is not. The tool treats an empty-bodied method as abstract. The same happened to stubs that raise NotImplementedError in the generated code
Member.kind, a property computed from kind_name
Both kind : MemberKind and kind_name : str as attributes, with no link to MemberKind
Loan refers to its copy by barcode text (chapter 9's object diagram)
Nothing: correctly, there is no link in the code
Refusal, an exception class used everywhere
A lone empty box, connected to nothing
Multiplicities, roles, constraints, and the reason for any design choice
None: the code does not contain them
Its package diagram is also a different thing from chapter 9's. It draws modules as boxes (desk, ports, model, jsonfile and so on) with 14 import arrows, where chapter 9's script reduced the same code to four packages and five arrows with rules attached. Neither is wrong; the second answers an architectural question and the first lists imports.
The Round Trip
The cleanest test of reverse engineering is to go diagram, code, diagram: reverse-engineer the generated code from above and compare the relationships with those in the original design:
"""Diagram -> code -> diagram. Reverse-engineer code with pyreverse and compare the result with the diagram the code was generated from.
py roundtrip.py (design.mmd against generated_library.py)
"""
import pathlib
import re
import subprocess
import uml2py
HERE = pathlib.Path(__file__).parent
PYREVERSE = HERE.parent / "pylintenv" / "Scripts" / "pyreverse.exe"
def expected_relationships(design: dict) -> set:
"""What the design says, as (kind, a, b). An attribute whose type is another class is not counted: the design wrote it as an attribute."""
out = set()
for c in design.values():
if c.base:
out.add(("inherits", c.name, c.base))
for i in c.realises:
out.add(("realises", c.name, i))
for t, _ in c.parts + c.refs:
out.add(("has", c.name, t))
return out
def found_relationships(mermaid: str) -> set:
out = set()
for line in mermaid.splitlines():
m = re.match(r"\s*(\w+) --\|> (\w+)", line)
if m:
out.add(("inherits", m.group(1), m.group(2)))
m = re.match(r"\s*(\w+) --([*o]) (\w+)", line) # 'Part --* Whole' and 'Part --o Whole': the whole is on the right
if m:
out.add(("has", m.group(3), m.group(1)))
m = re.match(r"\s*(\w+) --> (\w+)", line)
if m:
out.add(("has", m.group(1), m.group(2)))
return out
def compare(design_path, code_path, workdir) -> str:
design = uml2py.parse(pathlib.Path(design_path).read_text(encoding="utf8"))
out = pathlib.Path(workdir)
out.mkdir(exist_ok=True)
subprocess.run([str(PYREVERSE), "-o", "mmd", "-p", "rt", str(code_path)], cwd=out, capture_output=True, check=True)
recovered = (out / "classes_rt.mmd").read_text(encoding="utf8")
expected, found = expected_relationships(design), found_relationships(recovered)
lines = [f"design: {len(design)} classes, {len(expected)} relationships | recovered from the code: {len(found)}"]
lines += [f" in the design, NOT recovered : {e[1]} {e[0]} {e[2]}" for e in sorted(expected - found)]
lines += [f" recovered, NOT in the design : {f[1]} {f[0]} {f[2]}" for f in sorted(found - expected)]
lines += [f" recovered : {e[1]} {e[0]} {e[2]}" for e in sorted(expected & found)]
return "\n".join(lines)
if __name__ == "__main__":
print(compare(HERE / "design.mmd", HERE / "generated_library.py", HERE / "rt"))
design: 10 classes, 6 relationships | recovered from the code: 5
in the design, NOT recovered : Member has Loan
in the design, NOT recovered : Title has Copy
in the design, NOT recovered : DailyRateFine realises FineCalculator
recovered, NOT in the design : Copy has CopyStatus
recovered, NOT in the design : Member has Policy
recovered : Loan has Copy
recovered : JuniorPolicy inherits Policy
recovered : StandardPolicy inherits Policy
Three relationships made the whole trip (inheritance twice, and Loan to Copy). Three were lost: Member to Loan and Title to Copy (both collections), and the realisation of FineCalculator. Two appeared that were not in the relationship list: Member to Policy and Copy to CopyStatus. Those two are not errors: my design wrote policy: Policy as an attribute, and pyreverse correctly reads an attribute of another class's type as an association. The difference is notational, and it is a reminder that the same fact can be drawn as an attribute or as an arrow.
The lesson: a round trip is lossy. Whatever you draw by hand should not depend on a tool being able to recover it.
Model-Driven Generation, and Why It Faded
From general knowledge, not researched for this course: in the 2000s there was a strong push for model-driven development, where the model, not the code, was the source: tools generated whole applications from UML and you never edited the output (Fowler's "UML as programming language" mode from chapter 1). It faded for mostly practical reasons. Behaviour needed a code-like language anyway, so the model became a clumsy programming language. Generated code was awkward to read and debug. Keeping model and hand-written code in step was painful (the generation-gap problem above, at scale), and the heavy tools got in the way of ordinary tools: version control, diffing, code review, refactoring. What survived are the narrow forms: generating code from interface and schema definitions, ORM models, and API descriptions, where the input is small, precise and has no behaviour. Mine, above, is the same narrow form.
Keeping Diagrams Honest
A diagram that disagrees with the code is worse than none: it is trusted and wrong. The choices, from cheapest up:
Draw less, and say when. A small sketch dated at the top, kept as text next to the code, is honest about being a snapshot.
Generate it (pyreverse; chapter 9's package diagram; the object diagram), so it cannot drift, at the price of being only what the tool can read.
Check it. Chapter 7's conformance test checks a sequence diagram against calls; chapter 9's rules check package dependencies; here is the equivalent for class diagrams, comparing the operations on the hand-drawn chapter 6 diagram with the methods in the code:
"""Has the hand-drawn class diagram drifted from the code? Compare the operations on each class in the diagram with the methods in the code."""
import ast
import pathlib
import re
import sys
HERE = pathlib.Path(__file__).parent
def diagram_ops(path: pathlib.Path) -> dict[str, set[str]]:
ops, current = {}, None
for line in path.read_text(encoding="utf8").splitlines():
c = re.match(r"\s*class (\w+)", line)
if c:
current = c.group(1)
ops.setdefault(current, set())
continue
o = re.match(r"\s*\+(\w+)\(.*\)", line)
if o and current:
ops[current].add(o.group(1))
return ops
def code_ops(path: pathlib.Path) -> dict[str, set[str]]:
out = {}
for node in ast.parse(path.read_text(encoding="utf8")).body:
if isinstance(node, ast.ClassDef):
out[node.name] = {f.name for f in node.body if isinstance(f, ast.FunctionDef) and not f.name.startswith("_")}
return out
if __name__ == "__main__":
diagram, code = diagram_ops(HERE / "version_b.class.mmd"), code_ops(HERE / "library_b.py")
print(f"diagram: {len(diagram)} classes | code: {len(code)} classes")
for name in sorted(set(diagram) | set(code)):
if name not in code:
print(f" {name}: in the diagram, no such class in the code")
elif name not in diagram:
print(f" {name}: in the code, not in the diagram (methods: {sorted(code[name]) or 'none'})")
else:
gone, undrawn = sorted(diagram[name] - code[name]), sorted(code[name] - diagram[name])
if gone or undrawn:
print(f" {name}:" + (f" drawn but not in the code {gone}" if gone else "") + (f" in the code but not drawn {undrawn}" if undrawn else ""))
print(" (an empty report would mean the operations match exactly)")
diagram: 8 classes | code: 9 classes
Junior: in the code but not drawn ['fine_per_day', 'loan_limit']
Member: in the code but not drawn ['open_loans']
Refusal: in the code, not in the diagram (methods: none)
Standard: in the code but not drawn ['fine_per_day', 'loan_limit']
(an empty report would mean the operations match exactly)
It found no wrong operations, but it found that my chapter 6 diagram omitted some: Member.open_loans is not drawn, and the Junior and Standard overrides are not drawn (a deliberate convention, as above), plus Refusal, which I left out as noise. A check can list the differences; deciding which are deliberate is still a human job. The check is more useful when a change breaks something. Here someone renames check_can_be_lent to ensure_lendable in the code and does not touch the diagram:
Copy: drawn but not in the code ['check_can_be_lent']; in the code but not drawn ['ensure_lendable']
That is the signal you want in a build. Run it with the tests, and it fails the day the name changes, not the day someone is misled by the diagram.
What this check cannot catch
It compares names. It will not notice that a drawn operation's meaning changed, that a multiplicity is now wrong, or that an arrow's direction no longer matches the dependencies. For arrows, use the generated diagram or a dependency check; for behaviour, use tests.
Usual Mistakes
Mistake
Fix
Editing generated code
Generation gap: subclass in a hand-written file, or generate once and own the result
Expecting reverse engineering to recover the design
It recovers structure, not intent: no multiplicities, roles, constraints, or reasons
Trusting a tool's picture without reading its legend
pyreverse's * marked empty methods "abstract"; check against the code
A diagram nobody checks
Date it, generate it, or put a drift check in the build
Treating a skeleton as progress
Behaviour is the work; a generated skeleton is boilerplate, not design
Leaving the diagram silent on what the code needs
If a concrete subclass must implement an operation, say so, or the next reader (or generator) will not know
Mistakes while preparing this chapter
The generator had the plural bug from chapter 4. Its first output named the list of copies copys, the same "add an s" mistake I made in chapter 4's sentences, and I only saw it by reading the generated code. I had not carried the lesson into new code. It now maps a trailing "y" to "ies" and converts CamelCase to snake_case (job_parts). Takeaway: when you have made a mistake in one tool, grep your next tool for it.
I assumed pyreverse would draw the collection. I expected an arrow from Member to Loan. It drew only the text of the field type. I would not have noticed if I had not compared the picture with the code line by line. Takeaway: reverse-engineered diagrams are claims too; check them.
A refactoring script that broke its own file. I turned roundtrip.py into a function with a line-by-line text transformation and it failed with IndentationError. Rewriting the file by hand took less time than the script had saved. Takeaway: restructure code with an editor, not a string-munging script, and run it straight away.
The first design left a hole I did not see until the tests ran. The diagram drew the two concrete policies and no operations in them; the generated classes could not be created. It is in the chapter as a finding, but it was a surprise to me, and a good argument for generating code from a design early.
Hands-On Exercises
Exercise 1
Run the generator on the bike shop design in the solution file (an enumeration, an interface with an implementation, a composition and two associations). Leave the generated file alone and finish the behaviour by subclassing in a second file, with tests, including one that shows a required link cannot be omitted.
Reverse-engineer the bike shop code from exercise 1 with pyreverse and compare the relationships with the design. List what was lost and what was gained, and say which losses are the tool's limits and which are limits of the code itself.
Extend the drift idea from operations to attributes: compare the attributes in a class diagram with the fields in the code (allowing for the link fields the generator adds), show that an untouched generated file has no drift, and that a hand edit (renaming one field, adding another) is reported.
Class to dataclass; interface to Protocol (structural) or ABC; abstract to ABC; enumeration to Enum; generalisation to subclass; association to a field by multiplicity (1 required, 0..1 optional, many a list); composition = the whole creates the parts; constraints are code you must write
Forward generation gives structure and boilerplate, no behaviour; a diagram may omit what code needs (inherited operations the subclass must implement); never edit generated code, use the generation gap (subclass elsewhere)
pyreverse: shows classes, methods, types, inheritance; misses collections as associations, structural realisation, properties; marked an empty method abstract; a round trip is lossy
Model-driven generation as "the model is the program" faded; narrow generation (schemas, interfaces, ORM) survived (general knowledge)
Keep diagrams honest: draw less and date it, generate it, or check it (conformance, dependency rules, a drift check on names); a check compares names, not meaning
Java mapping given here was not compiled
Coming next
Object-Oriented Design with UML 11 (Patterns, Principles and Design Review): showing Strategy, Observer, State, Factory and Composite on a class diagram (linking to Design Patterns, not re-teaching), principles on diagrams, a design review checklist, spotting a god class, a cyclic dependency and an anemic model on a diagram, and refactoring the library on the diagram first and then in code.