Skip to content

Practice Lab 12 — Employee payroll hierarchy

Difficulty: Intermediate · Estimated time: 5 hours · Module 6: Inheritance

A payroll hierarchy with four kinds of employee, and one requirement that arrives late and breaks the hierarchy: a contractor, who is paid like an employee but is not one.


At a glance

Difficulty Intermediate
Estimated time 5 hours
Module Module 6 — Inheritance
Concepts practised Hierarchies with shared state, protected members, multilevel inheritance, overridden versus hidden operations, refactoring when the hierarchy is wrong, composition as the alternative
Prerequisite Practice Lab 11
Class design the four roles are given; the hierarchy is your decision to justify

Scenario

A payroll office pays:

  • salaried staff — a yearly salary, divided into twelve;
  • hourly staff — an hourly rate and the hours worked in a month;
  • commission salespeople — a base salary plus a percentage of sales;
  • contractors — an agreed monthly fee, no employment benefits, and no yearly salary at all.

Gross pay, tax and net pay are computed from these. The office also needs a monthly run that prints one line per person and a total.

The complication, which arrives in the middle of your work, is the contractor. The office insists that a contractor is paid like an employee, and the temptation is to derive one from the other. Whether that is honest is the design question of this lab.


Problem statement

Implement the payroll model. Requirements:

  1. Every payee has a name, an identifier, and a monthly gross pay.
  2. Salaried staff: yearly salary in whole pence, refused if not positive; monthly gross is the year's twelfth, with the remainder handling documented.
  3. Hourly staff: an hourly rate and hours worked; an hour count over a documented maximum is refused.
  4. Commission salespeople: a base salary and sales for the month; commission is a percentage of sales.
  5. Contractors: a monthly fee, and no yearly salary and no benefits.
  6. Tax: a documented rule applied to gross pay, identical for every payee — and one place only.
  7. A payroll run prints name, identifier, role, gross, tax and net, and a final total.
  8. A tax-rule change must not require editing any payee type.
  9. An invalid input (negative hours, sales, fee, salary) is refused with a message that names the field.
  10. Adding a fifth role later must be a new type plus one registration, not an edit to the tax rule.

Deliverable in payroll-design.md: the hierarchy you chose, with an is-a sentence for each relationship you used, the relationship you refused, and what each choice costs a caller.


Learning objectives

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

  • Choose between inheritance and composition where a role is paid like something but is not it.
  • Write a hierarchy whose shared behaviour is genuinely shared, and whose varying behaviour is small.
  • Keep an operation that applies to every payee (tax) in exactly one place.
  • Explain what a late requirement reveals about a hierarchy, and refactor when it reveals a mistake.
  • Document a hierarchy in writing, including the relationship you rejected.

Functional requirements

  1. All money in whole pence; no double in the domain (search for it and record the result).
  2. Each role's gross pay is computed by an operation the payroll run can call without asking the role.
  3. Tax is applied in one function, taking a gross amount, and it knows nothing about roles.
  4. The payroll run prints in a documented order, stable for equal names.
  5. A payee with an invalid rate or hours field is refused when it is created, not when it is paid.
  6. A contractor has no yearly salary field at all — not a zero one. Say how you enforced that.
  7. The monthly total equals the sum of the printed lines: verify that in the program, not by eye.
  8. Every operation that only reads is const.

Constraints

  • Header and source file per type family, plus main.cpp; explicit build list.
  • No virtual yet. Module 7 introduces it. In this lab, the payroll run must work by calling an operation that exists on a type you choose — and you must write down where that breaks.
  • No templates, no smart pointers.
  • No switch over the role anywhere; searched for and recorded.
  • No warnings.

Starter task

In payroll-design.md, before coding:

  1. Draw the hierarchy you intend. For each arrow, write the is-a sentence.
  2. For the contractor, write both candidate designs: derived from an employee base, and a separate type that composes a payment rule. Give each a one-sentence cost.
  3. Write down which operation the payroll run will call on each payee, and where it is defined.
  4. Predict what happens when the payroll run holds a base reference to a commission salesperson and asks for the gross pay. Record the prediction; case 8 will test it.

Implementation tasks

  1. Implement the payee types and their gross-pay computation.
  2. Implement the tax rule as one function, taking an amount.
  3. Implement the payroll run and the monthly total.
  4. Write main with the test cases, including the base-reference prediction.
  5. Add the contractor, following the design you chose — and if you chose inheritance and then regret it, refactor and record the change.
  6. Verify the total against the printed lines inside the program and print the check.

Test cases

# Action Expected result
1 Salaried staff, yearly 4,800,000 pence Monthly gross 400,000
2 Salaried staff, yearly 1,000,000 pence Monthly gross with your documented remainder rule
3 Hourly staff, 40 hours at 2,500 pence Gross 100,000
4 Hourly staff, hours over the maximum Refused at construction
5 Commission, base 200,000 plus 5% of 1,000,000 sales Gross 250,000
6 Contractor, fee 300,000 Gross 300,000; no yearly salary mentioned anywhere in the output
7 Tax on a gross of 400,000 The documented rule's result
8 Payroll run holding a base reference to a salesperson Your recorded prediction; note the difference if there is one
9 Total of a run of four payees Equal to the sum of the printed nets, verified in the program
10 Construct a payee with a negative fee Refused, naming the field
11 Print the run twice Identical output, same order

Boundary cases to run deliberately: zero sales; zero hours; a yearly salary not divisible by twelve; one payee of each role in the same run.


Expected behaviour

Payroll for 2026-04
  Idris CFO        salaried    gross   400000  tax    80000  net   320000
  Sana  Developer  hourly      gross   100000  tax    15000  net    85000
  Omar  Sales      commission  gross   250000  tax    45000  net   205000
  Hina  Contractor contract    gross   300000  tax    60000  net   240000
  total net: 850000
  check: printed lines sum to 850000 -> agree

Extension challenge

Add a benefit, available to employed staff but not to contractors: a pension contribution that is a percentage of gross, with a cap.

Record in payroll-design.md:

  • Where the benefit rule lives, and how it knows who is eligible — without a switch over the role.
  • Which candidate designs now look wrong, and why the contractor requirement should have warned you.
  • The change cost: files, operations, lines.
  • The sentence that decides, for any future role, whether it can have a benefit.

Debugging challenge

The monthly total disagrees with the printed lines by a few hundred pence:

int Payroll::total_net() const {
    int total = 0;
    for (const Payee& payee : payees_) {
        total += payee.gross_pay() - tax(payee.gross_pay() - benefits_);
    }
    return total;
}

int Payroll::net_of(const Payee& payee) const {
    return payee.gross_pay() - tax(payee.gross_pay());
}

Reproduce it and answer:

  1. Which two computations disagree, and on which payees?
  2. Which single rule did the total apply that the individual line did not?
  3. What is the design mistake underneath — two places computing net pay, or a benefits rule in the wrong place?
  4. Rewrite it so that the total is the sum of the same values the run prints, and say why that is a rule rather than a coincidence.

Reflection questions

  1. Was the contractor derived from an employee base, or composed with a payment rule? What made you choose, and what would reverse the decision?
  2. Which of your operations is the payroll run's single call into a payee? Where is it defined, and what breaks without virtual?
  3. Tax is one function that knows nothing about roles. Name a requirement that would force it to know, and what you would do instead.
  4. Which fields does a contractor not have, and how did you make their absence impossible rather than zero?
  5. The remainder rule for a yearly salary split into twelve: what did you choose, and who would care?
  6. If a fifth role arrived next month, what would you add, and what would you have to edit? Answer with a number.

Completion checklist

  • [ ] It builds from a documented command with -Wall -Wextra -Werror and no warnings
  • [ ] Every inheritance relationship has an is-a sentence, and the refused relationship is documented
  • [ ] No double in the domain — searched for and recorded
  • [ ] No switch over the role — searched for and recorded
  • [ ] Every role's gross pay is computed in one place
  • [ ] Tax is one function taking an amount, with no knowledge of roles
  • [ ] The contractor has no yearly-salary field rather than a zeroed one
  • [ ] The monthly total is verified against the printed lines inside the program
  • [ ] All eleven test cases run, with output recorded, including the base-reference prediction
  • [ ] The debugging challenge was explained before being fixed
  • [ ] payroll-design.md states the change cost of the fifth role

Related: Lab index · Module 6 — Inheritance · Previous: Practice Lab 11 · Next: Practice Lab 13 — Shape drawing system