Practice Lab 31 — A design-pattern application, assembled¶
Difficulty: Expert · Estimated time: 8 hours · Module 15: SOLID Principles and Design Patterns
Several patterns in one small system, chosen because the forces are there — and one deliberately not chosen, with the reason.
At a glance¶
| Difficulty | Expert |
| Estimated time | 8 hours |
| Module | Module 15 — SOLID Principles and Design Patterns |
| Concepts practised | Factory and strategy, observer, adapter, builder, dependency inversion, when not to use a pattern, documenting design decisions |
| Prerequisite | Practice Lab 30 |
| Class design | yours — with every pattern justified by a force |
Scenario¶
A university's graduation clearance system must assemble one student's clearance status from several independent checks, apply rules that change between faculties, and tell interested offices when a status changes. It must also read its data from two different systems: one modern exporter and one old library whose interface cannot be changed.
The brief names no patterns. That is deliberate: the four forces below are what should decide, and one of them may point the other way.
- Clearance is computed differently per faculty, and the rules change every year — without deploying a new build of the whole system if that is possible at this stage of the course, and cleanly if it is not.
- The checks are independent: library dues, hostel dues, lab breakage, sports, thesis submission. Some faculties skip some checks entirely.
- Offices subscribe to status changes. The records office wants every change; the library only wants changes involving dues; the student's advisor wants only final decisions.
- There is a legacy "OldReg" library with its own awkward types that must supply registrations.
- A clearance status has eight fields, most optional; constructing it positionally has already caused two bugs where fields were swapped.
Problem statement¶
Design and build the system.
- A design document first (
design.md), naming the forces and, for each, the structure that answers it — naming a pattern only after the force is described. One force must be answered without a pattern; say so and say why. - The system itself: clearance computation with per-faculty rules, independent checks, status change notification with different subscriptions, the legacy adapter, and a safe construction path for the eight-field status.
- The checks: at least five, each independently testable, each able to report not applicable as distinct from passed.
- The rules: the faculty differences must be data or behaviour selected at one point, and adding a faculty must not edit the computation.
- Notification: three subscription behaviours as above, including one that filters by field, and one that only receives final decisions.
- The adapter: the legacy library is used through an interface the system defines, with the legacy types translated at exactly one boundary; the legacy library itself is not edited.
- Safe construction: the eight-field status cannot be built with fields swapped — make the wrong construction either impossible or rejected, and demonstrate the failure.
- A decision log: every design decision with the alternative considered and why it lost. This is the deliverable that matters most; a working program with no decision log fails this lab.
In design.md:
- One section per force: the force, the structures available, the one chosen, and the cost accepted.
- The force answered without a pattern, and what a pattern would have cost there.
- The interfaces, drawn with a class diagram, and which dependency points at which abstraction.
- The one decision you expect to regret, and what would tell you it was wrong.
Learning objectives¶
By the end of this lab you will be able to:
- Read a set of forces and choose structures from them, rather than matching a description to a pattern name.
- Assemble several patterns in one system without the seams leaking into the callers.
- Use an adapter to keep a legacy dependency behind your own interface.
- Make an eight-field value type impossible to construct incorrectly.
- Write a design decision log that records the losing alternative, not only the winner.
- Identify one place where a pattern would be worse than the plain code, and say why.
Functional requirements¶
- Adding a faculty touches one file plus one selection point — recorded with a diff.
- All five checks run independently, and not applicable is not reported as passed anywhere.
- The three subscription behaviours work: all changes, dues-related only, final decisions only.
- New faculties can skip a check without editing the check.
- The legacy library is used through the system's own interface; its types appear in exactly one file. Searched for and recorded.
- The eight-field status cannot be constructed with two fields exchanged; a test demonstrates the rejection or the compile failure.
- No global mutable state: two clearances computed in the same run do not affect each other.
- The computation is dependably repeatable for the same input.
- Each of the five checks is exercisable from the top-level program with a stated input and outcome.
design.mdaccounts for every pattern used, one extra force, and one deliberate non-pattern.
Constraints¶
- Header and source per type family, with a documented build command.
- The legacy library is supplied as a small header/source pair with its own awkward names; it is compiled as is and never edited — a diff check is recorded.
- Runtime rule loading (a file or a table) is optional; if you use it, record what it costs.
- No warnings.
Starter task¶
In design.md, before coding:
- Write the four forces in your own words, each with the code smell it produces if unmet.
- For each force, write the two candidate structures and your choice, before naming any pattern.
- Choose the force to answer without a pattern, and write what a pattern would cost there.
- Sketch the interfaces and the dependency directions.
- Write the test that will demonstrate the swapped-field rejection.
Implementation tasks¶
- Define the system's interfaces: checks, rules, subscribers, registration source.
- Implement the five checks with three outcomes.
- Implement the per-faculty rules and the selection point.
- Implement the subscription behaviour, including the dues filter and the final-decision only case.
- Write the adapter over the legacy library, keeping its types in one file.
- Implement the eight-field status with a construction path that rejects a swap.
- Wire the top-level program: compute two clearances, notify, print the decision log's pattern list.
- Add a sixth faculty and a sixth check, and record both diffs.
Test cases¶
| # | Action | Expected result |
|---|---|---|
| 1 | Compute a clearance with all five checks passing | Cleared, with all five outcomes |
| 2 | A faculty that skips two checks | Those two not applicable, not passed |
| 3 | Library dues outstanding | Blocked, attributed to the right check |
| 4 | Record office subscription | Every change received |
| 5 | Library subscription | Only dues-related changes |
| 6 | Advisor subscription | Only the final decision |
| 7 | Registration from the modern source | All students present |
| 8 | Registration from the legacy source via the adapter | Identical set for the same data |
| 9 | Status built with two fields exchanged | Rejected, or impossible to compile — demonstrated |
| 10 | Add a sixth faculty | One file plus one selection point — diff recorded |
| 11 | Add a sixth check | One file plus one registration point — diff recorded |
| 12 | Two clearances in one run | Independent results; no shared mutable state |
| 13 | Search for legacy types outside the adapter file | None found |
| 14 | Search for a global mutable variable | None found |
Boundary cases to run deliberately: a student with no registrations; a faculty with no rules; a check that throws; no subscribers at all; a final decision reached with checks not applicable.
Expected behaviour¶
clearance S-2026-114 (engineering, skips sports):
library passed
hostel passed
lab blocked: 2 unreturned kits
sports not applicable (faculty policy)
thesis passed
-> blocked (1 blocking check)
notifications:
records office: 3 changes (open -> blocked -> open -> final)
library: 1 change (dues)
advisor: 1 change (final)
legacy adapter: 12 registrations, identical to the modern source for the same data
status with swapped fields: rejected by construction (Name/Id order enforced)
sixth faculty 'law': 1 new file + 1 selection line
legacy types outside adapter: none
Extension challenge¶
Make the per-faculty rules loadable at run time from a documented table, while keeping the checks in code.
Record in design.md:
- What the table holds and what it deliberately cannot express.
- How a malformed table is handled — the validation, and where it happens.
- What this buys (rules change without a rebuild) and what it costs (a new failure mode, no compiler help).
- Whether you would ship it, and the condition that decides it.
Debugging challenge¶
Two clearances computed in the same run start influencing each other after the third call:
class ClearanceService {
public:
Status compute(const Student& s) {
if (s.faculty() == "engineering") {
static std::vector<std::string> blocked; // remembers from the last call
...
}
}
};
Reproduce it — compute three clearances where the first has a blocker — and answer:
- What does the
statickeep, and for how long? - Which test case catches it, and what does the output show on the second and third clearances?
- Why is a function-local
staticespecially hard to see in a service class, compared with a member? - Which decision in
design.mdwould have to change to make this state legitimate, and what would you record about its lifetime?
Reflection questions¶
- Which force did you answer without a pattern, and what would the pattern have cost there?
- Naming patterns only after describing the force — did that change any decision? Which one?
- The library subscription filters by field. Where does the filter live, and how did the field get there?
- The adapter keeps legacy types in one file. What does that one boundary buy, and what does it cost per new legacy type?
- The eight-field status: what construction approach did you use, and what class of bug is now impossible? What class is still possible?
- Two decisions you would make differently in a system ten times the size, and why size changes them.
- Which pattern in the system is the weakest fit, and what would you replace it with if the requirement changed?
Completion checklist¶
- [ ] It builds from a documented command with
-Wall -Wextra -Werrorand no warnings - [ ]
design.mddocuments one section per force, each naming the structure before the pattern - [ ] One force is answered without a pattern, with the alternative's cost stated
- [ ] The class diagram shows the interfaces and the dependency directions
- [ ] Every design decision has its losing alternative recorded
- [ ] Adding a faculty and adding a check were both done, with diffs recorded
- [ ] Not applicable is distinct from passed in every check
- [ ] All three subscription behaviours work
- [ ] Legacy types appear in exactly one file — searched for and recorded
- [ ] The swapped-field construction is impossible or rejected — demonstrated
- [ ] No global mutable state — searched for and recorded
- [ ] All fourteen test cases run, with output recorded
- [ ] The debugging challenge was explained before being fixed
- [ ] I can name the one decision I expect to regret, and what would show it
Related: Lab index · Module 15 — SOLID Principles and Design Patterns · Previous: Practice Lab 30 · Next: Practice Lab 32 — Multi-module management system