Practice Lab 32 — Multi-module management system¶
Difficulty: Expert · Estimated time: 12–16 hours · Module 16: Advanced OOP and Capstone Development
The final laboratory. It is deliberately under-specified where a decision is yours, and strict about the things that make a program maintainable by somebody else.
At a glance¶
| Difficulty | Expert |
| Estimated time | 12–16 hours |
| Module | Module 16 — Advanced OOP and Capstone Development |
| Concepts practised | Multi-module layout, dependency direction, interfaces, ownership, persistence, error handling, automated tests, documentation, refactoring under a passing suite |
| Prerequisite | Practice Lab 31 |
| Class design | entirely yours |
Scenario¶
A regional equipment workshop needs to track its job orders. It is one system with several parts that will be developed by different people over a year:
- Catalogue — the equipment types the workshop services, with a stated service interval.
- Clients — the organisations and their contact people.
- Jobs — a job order for one piece of equipment, belonging to a client, with a state machine (received → assessed → awaiting parts → in service → tested → collected) and a cost.
- Parts — the parts consumed by jobs, with stock levels and reorder points.
- Scheduling — assigning jobs to technicians by skill and availability.
- Reporting — the workshop's weekly report and client statements.
- Persistence — saving and loading everything, and surviving a corrupt file.
Two facts shape the whole exercise. First, the parts system will be replaced by a purchased system next year,
so its boundary must be replaceable. Second, an auditor will ask, for any state transition, who could have
made it and what were the preconditions — so the state machine's rules must be explicit facts rather than
if conditions scattered through the code.
Problem statement¶
Design and build the system, in modules.
- A module structure with a stated dependency direction, and a rule you can check: no module includes another module's internal header, and there are no cycles. The build is documented and reproducible from one command.
- Interfaces at the removable boundary: the parts system is used through an interface the system defines. A second, in-memory implementation of that interface must be substitutable — since the purchased system is the third.
- An explicit state machine for jobs: the legal transitions, the preconditions of each, and the actor allowed to make it — data or a table, not scattered conditions. An illegal transition must be refused with a message naming the transition and the reason.
- An audit trail: every accepted transition records who, when, and the state before and after. The auditor's question must be answerable from the trail.
- Ownership and lifetime: state the ownership policy in one paragraph, and make it true in the code — including for the interfaces, which are never owned by the code that uses them.
- Persistence: save and load the whole system to a documented text format, with a round-trip test that compares every field, and a refusal — not a crash — for a truncated, corrupt, or version-mismatched file.
- Automated tests: a test program with assertions covering, at minimum, the state machine's legal and illegal transitions, the parts interface's two implementations, a persistence round trip, a corrupt file, and a reporting calculation. The suite must fail if you break the behaviour it covers — prove that by breaking something on purpose.
- A refactoring under the suite: after the suite passes, choose one area and restructure it, with the suite green before and after, and the diff recorded.
- Documentation: a
READMEfor the program — what it does, how to build it, how to run the tests, the module map and dependency direction, the ownership policy, and the format specification. A new developer must be able to add a module using only this document. - A design review document (
design-review.md): the five decisions you are least sure of, each with the alternative, why you chose yours, and the observation that would prove you wrong.
In design-review.md, additionally: the module map with dependency arrows, the state machine table, the
ownership policy, and the one module whose boundary you expect to need changing.
Learning objectives¶
By the end of this lab you will be able to:
- Lay out a multi-file C++ program with a stated dependency direction and no cycles.
- Put an interface at a boundary where an implementation will be replaced, and substitute one.
- Express a state machine as data with preconditions and actors, and refuse illegal transitions usefully.
- Persist a whole system with round-trip verification and hostile-input handling.
- Write a test suite that would catch a regression, and prove it by causing one.
- Refactor under a passing suite and report the change.
- Document a system so another developer can extend it.
Functional requirements¶
- The build compiles every module from one documented command, warning-free, including the tests.
- The dependency direction is stated and respected; a check for internal-header inclusion and for cycles exists (a script or a documented grep), with its output recorded.
- The parts interface has two implementations; switching between them is one line in
main. - Every state transition is checked against the table; each refusal names the transition and the reason.
- The audit trail answers "who, when, before, after" for any accepted transition, and is printed for one job end to end.
- Illegal transitions include: skipping a state, going backwards without the documented rule, a transition by an actor not permitted for it, and one with an unmet precondition (parts not in stock).
- Persistence round-trips every field including the audit trail, and refuses corrupt input with a message naming the line.
- The test suite runs as part of the documented build, prints a pass/fail summary, and exits non-zero on failure.
- Breaking one covered behaviour makes the suite fail — demonstrated, then reverted.
- The refactoring's diff is recorded, with the suite's result before and after.
- No module's header exposes another module's internal types.
- The README is sufficient: a developer could add a module from it alone — verified by writing down the steps you would follow and checking them against the document.
Constraints¶
- At least six modules, each with a public header and an internal header or source.
- The tests are a program; no third-party test framework is assumed.
- The persistence format is text, documented, with a version field.
- No global mutable state.
- No warnings; a clean build from scratch is the evidence.
Starter task¶
In design-review.md, before coding:
- Draw the module map with dependency arrows, and check for cycles on paper.
- Write the state machine table: state, event, precondition, actor, resulting state. Include the refusals.
- Write the ownership policy in one paragraph, including who owns the interfaces.
- Write the persistence format, field by field, with the version field and one example record.
- List the five tests that must exist, and what each would catch.
- Name the module boundary you expect to change, and the observation that would prove you wrong.
Implementation tasks¶
- Create the module skeleton and the build command; compile an empty program from it.
- Implement the catalogue and clients modules with their own tests.
- Implement the parts interface and two implementations; switch between them.
- Implement the jobs state machine from the table, with the audit trail.
- Implement scheduling with the technician skills as data.
- Implement reporting over the audit trail and the job costs.
- Implement persistence with the round trip and the corrupt-file refusals.
- Write the test suite and make it exit non-zero on failure; break something and confirm.
- Refactor one area under the suite; record the diff.
- Write the README and finish
design-review.md.
Test cases¶
| # | Action | Expected result |
|---|---|---|
| 1 | Build every module from one command | Clean, no warnings |
| 2 | Run the test suite | Pass summary; exit status 0 |
| 3 | Break a covered behaviour | The suite fails, with the failing case named; exit non-zero |
| 4 | Legal transition received → assessed by the front desk | Accepted; audit recorded |
| 5 | Illegal transition received → in service | Refused, naming the skipped state |
| 6 | Transition by an actor not permitted | Refused, naming the actor and the permission |
| 7 | Transition with parts out of stock | Refused, naming the precondition |
| 8 | Audit query for one job | Who, when, before, after for every transition |
| 9 | Switch the parts implementation | One line; behaviour otherwise identical |
| 10 | Persistence round trip | Every field and the audit trail identical |
| 11 | Load a truncated file | Refused with the line number; no crash |
| 12 | Load a version-mismatched file | Refused with both versions named |
| 13 | Client statement calculation | Matches a hand-computed expected value |
| 14 | Search module headers for another module's internal header | None |
| 15 | Dependency check for cycles | None reported |
| 16 | Add a module using only the README | Possible; the steps are recorded and the document's gaps listed |
Boundary cases to run deliberately: a job with no parts; a job that fails testing and returns to in service; a client with no jobs; an empty system saved and loaded; a state transition table applied at the first and last state.
Expected behaviour¶
build: 7 modules, 24 translation units, clean
tests: 46 assertions, 46 pass, exit status 0
break 'awaiting parts' precondition -> 1 failure at case 7, exit status 1 (then reverted)
job J-1042:
received -> assessed by front-desk 2026-03-02 09:14
assessed -> awaiting-parts by assessor 2026-03-02 11:40 (precondition: parts not in stock)
awaiting-parts -> in-service by technician T-7 2026-03-06 08:02 (parts P-3 x2 issued)
in-service -> tested by technician T-7 2026-03-06 15:20
tested -> collected by front-desk 2026-03-09 10:05
illegal received -> in-service: refused: 'received' cannot go directly to 'in-service' (skips assessed)
parts implementation switched to in-memory: 1 line, behaviour identical
persistence round trip: 12 jobs, 340 audit entries, all fields identical
corrupt file (line 4 truncated): refused: J-1042 record truncated at line 4
version mismatch: file version 1, program expects 2
refactoring: reporting extracted from the jobs module -> 4 files, 210 lines moved, suite green
dependency check: no cycles, no internal-header crossings
Extension challenge¶
Add a second presentation: the same system driven by a command language read from a file, with the existing interactive-ish caller untouched.
Record in design-review.md:
- The seam you used, and whether the existing caller needed any change — and what that says about the design.
- How the command language is validated, and what a malformed command produces.
- Whether the second caller revealed a missing abstraction in a module, and which.
- The change cost: files, operations, lines.
Debugging challenge¶
The persistence round trip passes for twelve jobs but the audit trail is short by one entry per job after a load-save-load cycle:
void Job::save(std::ostream& out) const {
out << id_ << '|' << state_ << '|' << client_id_ << '\n';
for (const auto& entry : audit_) {
out << " " << entry.who << '|' << entry.when << '\n'; // before/after not written
}
}
Reproduce it — save, load, save again, and compare the two files — and answer:
- Which fields are missing, and why does a single round trip look correct?
- What property of the round trip would you assert to catch this on the first trip rather than the second?
- Which test case in the table covers it, and what did it print?
- What does this bug say about testing a format by comparison rather than by field-by-field assertions?
Reflection questions¶
- Which module boundary did you guess would change, and does the evidence so far agree?
- The parts interface has two implementations. What did writing the second one teach you about the first interface, and what would you change about it now?
- The state machine is data. What did that buy over conditions, and what did it cost to set up? Which single transition made it worth it?
- Your test suite is a program. Which failure would it still miss, and what would you add next?
- The refactoring under the suite — how long did it take compared with the same refactoring without one, and what did the suite let you do that you otherwise would not have?
- What in
design-review.mdwould an experienced reviewer challenge first, and what is your answer? - If the purchased parts system arrives with an interface you cannot adapt cleanly, what is your plan — and what does that possibility say about the boundary you chose?
Completion checklist¶
- [ ] It builds from one documented command with
-Wall -Wextra -Werrorand no warnings, from scratch - [ ] At least six modules, each with a public header and internal implementation
- [ ] The dependency direction is stated, checked for cycles and internal-header crossings, with output recorded
- [ ] The parts interface has two implementations and is switched in one line
- [ ] The state machine is a table with preconditions and actors; illegal transitions are refused usefully
- [ ] The audit trail answers who, when, before and after
- [ ] Persistence round-trips every field and the audit trail, and refuses corrupt and mismatched files
- [ ] The test suite covers the five required areas and exits non-zero on failure
- [ ] Breaking a covered behaviour makes the suite fail — demonstrated and reverted
- [ ] One refactoring under a green suite, with the diff recorded
- [ ] The README lets a new developer add a module — verified and the gaps listed
- [ ]
design-review.mdhas the module map, the state table, the ownership policy and five least-certain decisions - [ ] All sixteen test cases run, with output recorded
- [ ] The debugging challenge was explained before being fixed
- [ ] No global mutable state
- [ ] I can answer the auditor's question for any transition from the trail alone
Related: Lab index · Module 16 — Advanced OOP and Capstone · Capstone project options · Previous: Practice Lab 31 · Next: choose a capstone project