Skip to content

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.

  1. 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.
  2. 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.
  3. The checks: at least five, each independently testable, each able to report not applicable as distinct from passed.
  4. The rules: the faculty differences must be data or behaviour selected at one point, and adding a faculty must not edit the computation.
  5. Notification: three subscription behaviours as above, including one that filters by field, and one that only receives final decisions.
  6. 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.
  7. Safe construction: the eight-field status cannot be built with fields swapped — make the wrong construction either impossible or rejected, and demonstrate the failure.
  8. 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

  1. Adding a faculty touches one file plus one selection point — recorded with a diff.
  2. All five checks run independently, and not applicable is not reported as passed anywhere.
  3. The three subscription behaviours work: all changes, dues-related only, final decisions only.
  4. New faculties can skip a check without editing the check.
  5. The legacy library is used through the system's own interface; its types appear in exactly one file. Searched for and recorded.
  6. The eight-field status cannot be constructed with two fields exchanged; a test demonstrates the rejection or the compile failure.
  7. No global mutable state: two clearances computed in the same run do not affect each other.
  8. The computation is dependably repeatable for the same input.
  9. Each of the five checks is exercisable from the top-level program with a stated input and outcome.
  10. design.md accounts 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:

  1. Write the four forces in your own words, each with the code smell it produces if unmet.
  2. For each force, write the two candidate structures and your choice, before naming any pattern.
  3. Choose the force to answer without a pattern, and write what a pattern would cost there.
  4. Sketch the interfaces and the dependency directions.
  5. Write the test that will demonstrate the swapped-field rejection.

Implementation tasks

  1. Define the system's interfaces: checks, rules, subscribers, registration source.
  2. Implement the five checks with three outcomes.
  3. Implement the per-faculty rules and the selection point.
  4. Implement the subscription behaviour, including the dues filter and the final-decision only case.
  5. Write the adapter over the legacy library, keeping its types in one file.
  6. Implement the eight-field status with a construction path that rejects a swap.
  7. Wire the top-level program: compute two clearances, notify, print the decision log's pattern list.
  8. 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:

  1. What does the static keep, and for how long?
  2. Which test case catches it, and what does the output show on the second and third clearances?
  3. Why is a function-local static especially hard to see in a service class, compared with a member?
  4. Which decision in design.md would have to change to make this state legitimate, and what would you record about its lifetime?

Reflection questions

  1. Which force did you answer without a pattern, and what would the pattern have cost there?
  2. Naming patterns only after describing the force — did that change any decision? Which one?
  3. The library subscription filters by field. Where does the filter live, and how did the field get there?
  4. The adapter keeps legacy types in one file. What does that one boundary buy, and what does it cost per new legacy type?
  5. The eight-field status: what construction approach did you use, and what class of bug is now impossible? What class is still possible?
  6. Two decisions you would make differently in a system ten times the size, and why size changes them.
  7. 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 -Werror and no warnings
  • [ ] design.md documents 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