Module 15 — Worked Examples¶
Seven programs in examples/module-15/, each folder with its own expected-output.txt. Build any of
them with the course's command:
Every program here is built the same way: the design that works, then the design that costs less to change, and a printed inventory of what a stated requirement would touch in each. That inventory is the module's evidence, because "better design" is a claim about the next change, and a claim about the next change can be counted.
Your prediction prompt for the whole page is one question: where would this change go?
1. One class, three reasons to change¶
examples/module-15/01-one-responsibility-per-class/main.cpp
Predict first: a LoanService lends items, computes fines and prints reports. A requirement
arrives: "students get a 10-day grace period." How many functions inside the class must be read
before you can be sure the change is safe?
Part A states the responsibility sentence; Part B runs the split design against the original for the same four loans:
BEFORE - LoanService::reportFor("Ayesha"), the old class:
Algorithms: £0.75
Databases: £1.75
total: £2.50
AFTER - FineReport::forMember("Ayesha", registerOfLoans):
Algorithms: £0.75
Databases: £1.75
total: £2.50
Identical, line for line - which is what makes this a refactoring and
not a rewrite.
What to notice. The output is identical, and the change inventory is not:
BEFORE: count: 1 file, 3 responsibilities at risk, 2 functions edited
AFTER: count: 1 file, 1 responsibility, 1 class added
One file in both cases - and that is the honest version of the claim.
The split does not reduce the COUNT here; it reduces what is at risk inside the file.
That honesty is the point. A student who claims the split "touched fewer files" is overstating a demonstration that contradicts them. Part D lists when the split is the wrong answer, which is the half of the principle that is usually left out.
2. The conditional that grows, and the hierarchy that lies¶
examples/module-15/02-growing-conditional-to-polymorphism/main.cpp
Predict first: a fourth item kind (a thesis) is added to the switch-based version and to the polymorphic version. In each, count the edits.
Change inventory for a fourth kind (a thesis, say):
ItemKind one enumerator
loanPeriodDays a new case <- site 1
finePencePerDay a new case <- site 2
describeLegacy a new case <- site 3
4 edits in 1 file.
...
Change inventory for the same fourth kind:
Equipment one new class implementing Lendable
0 edits to Book, Dvd, or any caller of them
1 edit where new kinds are registered - the factory
What to notice in Part C, which is the part worth reading slowly. The obvious hierarchy — a base
that promises every item can be lent, with a ReferenceCopy derived from it — produces this:
A client written against the base type is handed both items:
threw: reference copies are read in the library, never lent
The client used only what the base promised. The derived class genuinely cannot be lent. The fault is
in the abstraction, and the fix is a better one — a LendableCapability interface that only lendable
types implement — which turns the run-time throw into a compile error, checked in the program by two
static_asserts.
3. Interfaces and their owners¶
examples/module-15/03-interfaces-and-dependencies/main.cpp
Predict first: a report generator uses two operations of a six-operation interface. How many stub implementations must a read-only catalogue write, and which of those stubs is most likely to be called by accident?
what ReadOnlyCatalogue had to write:
operations it can honour: 2 (addItem, allTitles)
operations it must stub: 4 (lend, giveBack, emailMember, printReceipt)
...
what SnapshotCatalogue had to write:
operations it can honour: 2
operations it must stub: 0
Then Part C and D make the second principle visible as a missing capability rather than an error:
Part C: remind("ayesha@example.edu", 3) -> reminder sent
Where did that text go? Into a ConcreteEmailer constructed inside the
function. A test cannot intercept it...
Part D: assertions the test can now make:
messages recorded: 2
ayesha@example.edu <- You have an overdue item (3 days)
What to notice. The policy in Part C is not broken. Its logic is simply untestable, because it constructs its own dependency, and that is a design cost expressed as an inability to write a test — which is exactly the cost Module 16's testing chapter will ask you to eliminate.
4. A factory, and a strategy¶
examples/module-15/04-factory-and-strategy/main.cpp
Predict first: four catalogue records arrive — two valid, one with an unknown kind, one with an empty title. What does each look like from a caller's point of view, and where does the validation live?
create("book", "Compilers") -> Compilers [book, 21 days]
create("dvd", "Blade Runner") -> Blade Runner [dvd, 7 days]
create("journal", "Nature") threw: record rejected: unknown kind 'journal'
create("book", "") threw: record rejected: no title
What to notice. Part B adds a fifth kind in one class and one registration line, with no reader of records touched — and the validation the new kind gets is the validation that already existed. Parts C and D do the same for the lending rule, and the change inventory is the argument:
Change inventory for 'postgraduates get staff limits':
a new subclass of LendingRule, if the rule genuinely differs;
one line where rules are chosen (the composition root);
0 edits to Desk, Item, Member, or the factory.
Part E lists when each pattern is the wrong answer, including the case that applies most often to student code: a "strategy" that carries one integer has no variation behind it.
5. The observer, and who decides an observer is alive¶
examples/module-15/05-observer-and-lifetimes/main.cpp
Predict first: three subscribers are registered, one of them is a local that goes out of scope, and then an announcement is made. How many subscribers are notified — and how did the subject find out?
[board] "Compilers": notified 3, pruned 0, subscriptions now 2
...
subscriptions before the next announcement: 2
[board] "Algorithms": notified 1, pruned 1, subscriptions now 1
subscriptions after: 1 - the dead one was pruned, and nothing was called after destruction.
What to notice. The subject was not told that a subscriber died; it holds weak references, so it can find out. Part C shows a subscriber unsubscribing during a notification, and the walk finishing anyway because the announcement iterates a snapshot:
desk terminal unsubscribed itself: yes
terminal updates seen: 1 - one, from the announcement during which it left.
Part D writes out the naive version — std::vector<Subscriber*>, subscribe(&local) — and says why it
is not run here: calling a destroyed object is undefined behaviour, and a demonstration whose output is
undefined cannot be evidence. Part E gives the three designs and their costs, which is what a design
review of an observer subsystem should contain.
6. Adapter, builder — and a singleton, implemented and then declined¶
examples/module-15/06-adapter-builder-and-not-singleton/main.cpp
Predict first: a third-party, C-style catalogue API must be used by a system whose interface takes
std::string. Where does the character buffer live, who translates the error code, and how many files
mention the legacy type afterwards?
through the interface: Compilers, Blade Runner
and when legacy said 'no': legacy catalogue refused: full at 16 items
The builder answers a different question — a loan request with four required and four optional parts:
incomplete request threw at build(): no member named
full: "Compilers" for Ayesha (21 days, post, email on, note: renewal of an existing loan)
minimal: "Blade Runner" for Bilal (7 days, collect at desk, email off)
Then the part that matters. The singleton is implemented properly — one instance, thread-safe initialisation, observable sameness — and then declined, with four named costs and a working alternative:
the same instance every time? yes - that is what the singleton is for, and it works
...
1. HIDDEN DEPENDENCY. `FineCalculator` needs rates, and its interface does not say so.
2. UNTESTABLE. ...
the alternative - one instance, created in main and injected: 60p
and the test that was impossible before: a zero rate gives 0p
What to notice. The rejection is written down as a design note — considered, why not, instead, its cost, revisit if — because a refusal with a reason is reviewable and "we did not need it" is not.
7. The same requirement, with and without patterns¶
examples/module-15/07-over-abstraction/main.cpp
Predict first: "compute the fine for a loan" is implemented as a four-parameter function and as five collaborating types. Which one would you rather be handed for maintenance — and what would have to be true for the answer to be the other one?
Both correct. Then the cost, in the terms that matter:
to answer 'what is the fine for a staff DVD?', version A needs 4 lines of
arithmetic; version B needs the reader to follow a registry, a factory, an
id string built by concatenation, and a class per combination.
What to notice. Part C is the part students skip: it lists the conditions under which version B is the right answer — rules as configuration, separately reviewed rule owners, rules that must be substituted in tests, a large sparse combination space. Part D is the evaluation itself: the decision, the cost of the chosen option, the cost of the rejected one, and what would reverse it. That is the shape Lab 15's design report requires, and the shape of the capstone's "one decision I would reverse".
Continue¶
Previous: Concepts and explanations · Next: Misconceptions and mistakes · Module home: Module 15