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:
- Every payee has a name, an identifier, and a monthly gross pay.
- Salaried staff: yearly salary in whole pence, refused if not positive; monthly gross is the year's twelfth, with the remainder handling documented.
- Hourly staff: an hourly rate and hours worked; an hour count over a documented maximum is refused.
- Commission salespeople: a base salary and sales for the month; commission is a percentage of sales.
- Contractors: a monthly fee, and no yearly salary and no benefits.
- Tax: a documented rule applied to gross pay, identical for every payee — and one place only.
- A payroll run prints name, identifier, role, gross, tax and net, and a final total.
- A tax-rule change must not require editing any payee type.
- An invalid input (negative hours, sales, fee, salary) is refused with a message that names the field.
- 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¶
- All money in whole pence; no
doublein the domain (search for it and record the result). - Each role's gross pay is computed by an operation the payroll run can call without asking the role.
- Tax is applied in one function, taking a gross amount, and it knows nothing about roles.
- The payroll run prints in a documented order, stable for equal names.
- A payee with an invalid rate or hours field is refused when it is created, not when it is paid.
- A contractor has no yearly salary field at all — not a zero one. Say how you enforced that.
- The monthly total equals the sum of the printed lines: verify that in the program, not by eye.
- Every operation that only reads is
const.
Constraints¶
- Header and source file per type family, plus
main.cpp; explicit build list. - No
virtualyet. 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
switchover the role anywhere; searched for and recorded. - No warnings.
Starter task¶
In payroll-design.md, before coding:
- Draw the hierarchy you intend. For each arrow, write the is-a sentence.
- 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.
- Write down which operation the payroll run will call on each payee, and where it is defined.
- 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¶
- Implement the payee types and their gross-pay computation.
- Implement the tax rule as one function, taking an amount.
- Implement the payroll run and the monthly total.
- Write
mainwith the test cases, including the base-reference prediction. - Add the contractor, following the design you chose — and if you chose inheritance and then regret it, refactor and record the change.
- 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
switchover 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:
- Which two computations disagree, and on which payees?
- Which single rule did the total apply that the individual line did not?
- What is the design mistake underneath — two places computing net pay, or a benefits rule in the wrong place?
- 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¶
- Was the contractor derived from an employee base, or composed with a payment rule? What made you choose, and what would reverse the decision?
- Which of your operations is the payroll run's single call into a payee? Where is it defined, and what
breaks without
virtual? - Tax is one function that knows nothing about roles. Name a requirement that would force it to know, and what you would do instead.
- Which fields does a contractor not have, and how did you make their absence impossible rather than zero?
- The remainder rule for a yearly salary split into twelve: what did you choose, and who would care?
- 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 -Werrorand no warnings - [ ] Every inheritance relationship has an is-a sentence, and the refused relationship is documented
- [ ] No
doublein the domain — searched for and recorded - [ ] No
switchover 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.mdstates the change cost of the fifth role
Related: Lab index · Module 6 — Inheritance · Previous: Practice Lab 11 · Next: Practice Lab 13 — Shape drawing system