Skip to content

Practice Lab 8 — Appointment slot with validated booking

Difficulty: Intermediate · Estimated time: 4 hours · Module 4: Encapsulation and Access Control

The rule that makes this lab hard: two objects must agree. Validation in one object is a check; validation across two is a design.


At a glance

Difficulty Intermediate
Estimated time 4 hours
Module Module 4 — Encapsulation and Access Control
Concepts practised Invariants spanning objects, validation at the boundary, information hiding, interface design, refusing versus correcting, const correctness
Prerequisite Practice Lab 7
Class design yours — the requirements name the rules, not the classes

Scenario

A clinic books appointments by slot: a doctor, a date, a start time, a duration in minutes, and a capacity. A slot is free, partly booked, or full, and a waiting list exists for full slots.

Three rules come from the clinic's manager, and she has seen each of them broken by the previous system:

  1. An appointment is never double-booked: the same patient never holds two appointments in the same slot.
  2. A slot is never overbooked: the booked count never exceeds the capacity.
  3. A booking is never made in the past — and "the past" is today's date, which the program must be told rather than reading from the clock inside a rule.

The previous system let the receptionist set a slot's booked count directly, which is how rule 2 was broken. That setter is the thing you are not allowed to write.


Problem statement

Design and implement a booking model for one doctor's day, with at least:

  • something that represents a slot — doctor, date, start time, duration, capacity, and the bookings made in it;
  • something that represents a booking — a patient and the slot it is in;
  • today supplied from outside, never read inside a rule.

Operations the clinic needs:

  1. Create a slot, refusing a non-positive duration or capacity.
  2. Book a patient into a slot, refusing when the slot is full, when the patient already holds a booking in it, or when the slot is in the past.
  3. Cancel a booking, and offer the freed place to the waiting list before anyone else.
  4. Join a waiting list for a full slot, refusing a duplicate entry.
  5. Report a slot's free places and whether it is full.
  6. Print a doctor's day: every slot in time order, with its bookings.
  7. Print a patient's appointments, in time order, across slots.

You choose the classes, their names and their operations. The requirements are the contract; the design is yours. Write it down before writing code.


Learning objectives

By the end of this lab you will be able to:

  • Design several classes whose rules interact, and say which class owns which rule.
  • Express a rule that spans two objects, and enforce it in exactly one place.
  • Take a contextual value (today) as a parameter instead of reading it inside a rule, and explain what that makes testable.
  • Decide between refusing an operation and correcting the input, and defend the choice.
  • Distinguish a waiting list's order rule from a slot's capacity rule, and say why they live in different places.

Functional requirements

  1. A booking is always inside exactly one slot, and a booking never outlives its slot.
  2. The booked count is never set directly by any caller; it changes only through booking and cancelling.
  3. Booking refuses: a full slot; a patient already in that slot; a slot on a past date; an unknown slot.
  4. Each refusal names its reason, and names the numbers where a limit was involved.
  5. Cancelling a booking that does not exist is refused. Cancelling frees exactly one place.
  6. A freed place goes to the waiting list first, in the order the entries were added, and only then to a new caller.
  7. A patient cannot appear twice on the same waiting list.
  8. A slot's capacity is never above a documented maximum (choose one and document it).
  9. Printing a doctor's day is time-ordered and stable for equal times by a documented rule.
  10. Every query is const, and a const day can be printed.

Constraints

  • A header/source split; build with an explicit file list.
  • No setter for the booked count or the waiting list. This is the lab's central constraint.
  • Today is a parameter of every operation that needs it. time(), std::chrono::system_clock::now() and std::time must not appear in your code — search for them and record the result.
  • No inheritance, no templates, no smart pointers.
  • Dates as int in YYYYMMDD form, times as minutes since midnight. Document both.
  • No warnings.

Starter task

Write design.md with:

  1. Your classes, each with a one-sentence responsibility.
  2. For each of the three manager rules: the class that owns it, and the single operation that enforces it.
  3. The waiting list's order rule, stated as a sentence, and where it is enforced.
  4. Where "today" enters your program, and every operation that takes it.
  5. The one thing you deliberately chose not to model, and why it is not needed yet.

Then write the declarations only, and check that no public operation can set a booked count.


Implementation tasks

  1. Implement the slot and booking types, with the refusals.
  2. Implement the waiting list and the offering rule.
  3. Implement the two reports (a doctor's day, a patient's appointments).
  4. Write main to walk through the test cases in order, printing a line per action.
  5. Run the boundary cases, especially the same-day and next-day ones.
  6. Record in design.md what changed in your design while implementing — there is always something.

Test cases

# Action Expected result
1 Create a slot with capacity 0 Refused
2 Book two different patients into a capacity-2 slot Both accepted; free places 0
3 Book a third patient Refused: full
4 Book the same patient twice into one slot Refused: already booked
5 Book into a slot dated before today Refused: in the past
6 Book into a slot dated exactly today Accepted — today is not the past
7 Join the waiting list twice with the same patient Second entry refused
8 Cancel a booking in a full slot with a waiting list The first waiting patient is booked automatically; free places stay 0
9 Cancel a booking that does not exist Refused
10 Print the day with three slots added out of order Printed in time order
11 Print a patient's appointments across two slots In time order
12 Attempt to set a booked count from main Does not compile; the message is recorded

Boundary cases to run deliberately: capacity 1; a booking on the last place; cancelling the only booking in an empty slot; two slots at the same time; a waiting list of one.


Expected behaviour

today = 20260414
slot A  20260414 09:00  30 min  capacity 2   booked 0/2   free 2
book  A  patient 101 -> accepted   booked 1/2  free 1
book  A  patient 102 -> accepted   booked 2/2  free 0
book  A  patient 103 -> refused: slot is full (2 of 2)
join  A  patient 103 -> waiting, position 1
cancel A patient 102 -> booked 1/2; patient 103 booked from the waiting list, free 0
book  A  patient 101 -> refused: patient 101 already has a booking in this slot
book  B 20260413 09:00 -> refused: 20260413 is before today (20260414)
patient 101 has 1 appointment: 20260414 09:00

Extension challenge

Add doctor availability windows: a doctor is available from a time to a time, and a slot must lie entirely inside one.

Record in design.md:

  • Where the window rule lives, and how many operations consult it.
  • Whether a window belongs to the doctor, to the day, or to a separate scheduler — with the cost of each choice in one sentence.
  • What happens to existing slots when a window shrinks, and which of your rules decides it.
  • The change cost of the extension: how many files, and how many operations.

Debugging challenge

The clinic manages to double-book one patient:

bool book(int patient, int slot_index, int today) {
    if (slots_[slot_index].date < today) {
        std::cout << "refused: in the past\n";
        return false;
    }
    if (slots_[slot_index].booked >= slots_[slot_index].capacity) {
        std::cout << "refused: full\n";
        return false;
    }
    slots_[slot_index].patients[slots_[slot_index].booked] = patient;
    ++slots_[slot_index].booked;
    return true;
}

Reproduce it — two bookings by the same patient in one slot — and answer:

  1. Which rule is missing, and which of the manager's three rules is it?
  2. Why does the slot's capacity check not catch it?
  3. Where should the duplicate check live: in the booking operation, in the slot, or in the patient? Answer with what each placement implies about who knows about whom.
  4. If the patient also has appointments in other slots, does your fix still work? Say what you checked.

Reflection questions

  1. Which of your classes owns the "is this in the past" rule, and which operation passes today in? What would break if the rule read the clock itself?
  2. The waiting list is offered a freed place first. What happens if the waiting patient is no longer eligible, and how did your design discover that?
  3. You were not allowed to write a setter for the booked count. Which caller would have wanted one, and what does your design offer them instead?
  4. Two rules — capacity and duplicate booking — are enforced in different places. Is that a design smell? Justify your answer with what each place knows.
  5. Which of your operations returns a collection by value, and how big can that copy get?
  6. If the clinic added a second doctor whose slots share a waiting list, which classes would change and how many operations would follow?

Completion checklist

  • [ ] It builds from a documented command with -Wall -Wextra -Werror and no warnings
  • [ ] design.md names a class and a single operation for each of the three manager rules
  • [ ] No setter for a booked count or a waiting list — proved by an attempted call that does not compile
  • [ ] No clock read inside a rule — grep for time(, now() and std::chrono and record the result
  • [ ] Booking refuses all four invalid cases, each with its own reason
  • [ ] A freed place goes to the waiting list first — demonstrated with output
  • [ ] A const day can be printed
  • [ ] All twelve test cases run, with output recorded
  • [ ] Boundary cases run, including a slot dated exactly today
  • [ ] The debugging challenge was explained before being fixed
  • [ ] I can say which class knows about which other class, and why that direction is right

Related: Lab index · Module 4 — Encapsulation · Previous: Practice Lab 7 · Next: Practice Lab 9 — Money type with full operator support