Skip to content

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:

g++ -std=c++17 -Wall -Wextra -Werror main.cpp -o program
./program

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?

  Compilers / book / student, 4 days overdue:
    version A (a function):        40p
    version B (five types):        40p

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