Module 15 — Lab 15: patterns where the problem is real, and documented where they are not¶
Levels: [L5 Design → L8 Evaluate] · Extends: Lab 14 · Time: 5–6 hours
The scenario¶
The library system works, and it is about to grow in three directions at once. The campus wants equipment loans tracked alongside books (a new kind of item). Fines are to depend on the member's category as well as the item's kind (a rule that varies). And members want to be told when a held item becomes available (something that has to happen somewhere, and currently happens nowhere).
Three requirements, and each one, taken through Lab 14's code, touches more than one place. That is the finding this lab acts on: not that the system is wrong, but that the next three changes are not local.
You are going to add a factory for item creation, a strategy for the lending rule, and an observer for notifications — each justified by one of those requirements — and you are going to document one pattern you considered and rejected.
★ The deliverable that is marked first is the change-cost measurement, before and after: "adding a new kind used to touch four sites; now it touches one class and one registration line." That number is the only evidence that the indirection was worth its cost.
What you start from¶
Your Lab 14 program, with these three places to work on:
// 1. item creation, duplicated across every reader of a record
if (kind == "book") { items_.push_back(std::make_unique<Book>(title)); }
else if (kind == "dvd") { items_.push_back(std::make_unique<Dvd>(title)); }
else { throw std::invalid_argument("unknown kind"); }
// 2. the lending rule, inside the operation that lends
int days = 21;
if (member.kind == MemberKind::Staff) { days = 60; }
if (member.kind == MemberKind::External) { days = 7; }
if (month == 8 && vacation) { days = 90; }
// 3. notification: nothing exists, and the requirement is that members be told
Requirements¶
- A factory for item creation, with validation in one place and ownership expressed in its return
type —
std::unique_ptr<Item>, not a raw pointer. (Interacts with Module 11: the factory decides ownership on behalf of every caller, so it must say what it decided.) - A strategy for the lending rule, injected rather than selected by a conditional, with at least two implementations (the standard rule and the vacation rule, or the standard rule and a member-category rule). (Interacts with Module 7: the rule is substituted through a reference, and the interface needs a virtual destructor.)
- An observer for notifications, with an explicit answer to who unregisters, and when. The dangling-observer bug from the worked example must be avoided by design, and the design note must name which of the three answers you chose and what it cost. (Interacts with Module 9: this is the lifetime question in a new costume.)
- A UML class diagram of the whole system, matching the declarations, with composition, aggregation and association notated correctly and multiplicities marked.
- One documented rejection: a pattern you considered for a problem that did not need it, with the reason you declined. (Interacts with Module 14: the simpler alternative is often already in the standard library, or in a plain function.)
- The change-cost measurement: for a stated requirement, the files or sites it touched before and the number it touches now.
- A one-page design report: the three decisions and the requirement behind each, the costs accepted, and one decision you would reverse.
- It builds with no warnings using the course's compile command.
The change-cost measurement (requirement 6 in detail)¶
This is the lab's evidence, and it is a table, not a sentence.
| Requirement | Sites touched before | Sites touched now | What it cost |
|---|---|---|---|
| Add a fifth kind of item | ItemKind ×1, three switches, the record reader = 5 |
one class + one registration line = 2 | one interface, one factory, one registry entry |
| Add a member category with different limits | the rule, the limit check, the report = 3 | one LendingRule subclass + one line at the composition root = 2 |
one parameter threaded through Desk |
| Notify a new kind of subscriber | not possible (no notification point) | one class implementing Subscriber, one registration = 2 |
a list, a lifetime rule, and a pruning pass |
Fill it in before you refactor and again afterwards. Two entries will surprise you — one where the count barely changed (a factory that adds a class to save an edit is roughly break-even at the first kind) and one where it changed a lot. Both are findings, and the honest report includes the one that did not pay off.
What to deliver¶
- The patterned system, building and running.
- The UML diagram, matching the code.
- The change-cost table, before and after.
- The design note for each pattern: the requirement, the decision, the cost accepted.
- The rejection note: the pattern declined, the problem it was considered for, and why.
- The design report, ending with one decision you would reverse and what would have to change.
Self-assessment checklist¶
It builds and runs
- [ ] Compiles with
g++ -std=c++17 -Wall -Wextra -Werrorwith no warnings - [ ] All Lab 14 behaviour is unchanged, verified by comparing output
It is correct
- [ ] Every interface has a virtual destructor
- [ ] No implementer writes a stub it cannot honour
- [ ] No derived type throws from an inherited operation
- [ ] Nothing is notified after it is destroyed, and the mechanism is a design choice, not luck
- [ ] Ownership from the factory is expressed in its return type
It is designed
- [ ] Every class's responsibility is one sentence, without "and"
- [ ] The diagram matches the declarations, including multiplicities
- [ ] Every pattern names the requirement that justified it
- [ ] The dependency arrows point at interfaces the policy owns, not at concrete implementations
- [ ] I can name two principles that pulled against each other here, and which I favoured
It is verified
- [ ] The change-cost table has numbers for both before and after
- [ ] The behaviour comparison before/after the refactoring is recorded
- [ ] At least one pattern was considered and declined, in writing
- [ ] The design report names a decision I would reverse
Common ways this lab goes wrong¶
| Symptom | Cause |
|---|---|
| The factory is a class wrapping one constructor | No variation to centralise; the pattern was applied before the problem existed |
| Adding a kind still edits several files | The conditional moved into the factory, and the factory has a switch that also exists elsewhere |
| The strategy is a class holding one integer | Indirection with no variation behind it |
| A notification crashes after the subscriber is gone | The subject holds Subscriber* and nobody unregisters; see mistake M3 of Misconceptions |
warning: deleting object of abstract class type ... non-virtual destructor |
An interface without virtual ~Interface() = default; |
| The change-cost table shows no improvement | Possibly the honest result: report it, and say which decision you would reverse |
| Every class has exactly one method and one caller | Responsibilities split past the point where anything has a name |
| The design report has no weakness in it | The evaluation was not done — there is always one |
Reference solution¶
No solution file is published for this lab. It is a design you defend, and the decisions are the work: which pattern each requirement justifies, which pattern you considered and declined with a reason, and what each choice costs the next feature. Declining a pattern on purpose is marked as highly as using one.
The model code is in examples/module-15/, one program per idea, each already walked
through on the concepts page where it is used:
| Program | What it models |
|---|---|
01-one-responsibility-per-class |
The first design question, asked of a class that works perfectly |
02-growing-conditional-to-polymorphism |
The conditional that grows with every new kind, and the contract that replaces it |
03-interfaces-and-dependencies |
Two principles that are about interfaces rather than classes |
04-factory-and-strategy |
Two patterns, each introduced only where a requirement asks for it |
05-observer-and-lifetimes |
The observer pattern, and the question every observer design has to answer |
06-adapter-builder-and-not-singleton |
Three more patterns, including one the program deliberately does not use |
07-over-abstraction |
The same requirement implemented twice: once over-abstracted, once not |
The change-cost table is the part worth comparing against your own answer, because it is the version of this lab the module is actually asking for.
Continue¶
Previous: Exercises · Next: Challenge problems · Module home: Module 15