Functions Refactoring Exercises (10)
← Module home · Every exercise: baseline first, one extraction per step, compare after each step — the walkthrough checklist. Solutions give the target shape and the extraction order, not a lecture.
R1 ★ — The welcome-wagon main
int main() {
std::cout << "==============================\n";
std::cout << " STUDENT GRADE CALCULATOR\n";
std::cout << "==============================\n";
std::cout << "Enter marks for 5 subjects:\n";
// ... reads 5 marks, prints each with '-' padding ...
std::cout << "==============================\n";
}
Three print-blocks repeat the banner. Extract; main should read as three sentences.
Target:
void printBanner(const char* title); // one banner machine, title parameter
void readMarks(int& m1, int& m2, int& m3, int& m4, int& m5);
void printReport(...);
// main: printBanner("STUDENT GRADE CALCULATOR"); readMarks(...); printReport(...);
Order: banner first (pure output, zero logic), then readers, then the report.
R2 ★ — The inline climber
int main() {
int n;
std::cin >> n;
int total = 0;
for (int i = 1; i <= n; i = i + 1) total += i;
std::cout << "Sum: " << total << '\n';
}
Extract the sum. Name the machine the way a mathematician would.
Target: int sumTo(int n) — the S4 loop behind a name. main becomes read → compute → print. The test table writes itself: n=1, n=0 (empty-loop answer), n=100 (5050).
R3 ★★ — The duplicated discount
Two programs in one file (a shop and a canteen) each contain:
if (amount >= 1000) amount = amount * 0.90;
…but with different thresholds (canteen: 500, rate 0.95). Extract one machine both can call.
Target:
double applyDiscount(double amount, double threshold, double rate) {
return (amount >= threshold) ? amount * rate : amount;
}
Baseline both callers before touching anything; after extraction both behaviours must be byte-identical. The house-shopping lab uses exactly this shape.
R4 ★★ — The shadowed total
double total = 0; // global "for convenience"
void addItem(double price) { total += price; }
void printTotal() { std::cout << "Total: " << total << '\n'; }
int main() {
addItem(50); addItem(30);
printTotal();
return 0;
}
De-globalise. (main owns the state; functions own the maths.)
Target:
double addItem(double total, double price) { return total + price; }
void printTotal(double total);
// main: double total = 0; total = addItem(total, 50); total = addItem(total, 30); printTotal(total);
This is D5 at lab scale — baseline before, identical after.
R5 ★★ — The three-lab reader
You’ve copied the fail/clear/ignore reader into three different labs’ programs. Consolidate into the S24 machine and state the two wording decisions.
Target:
int readInRange(const char* prompt, int lo, int hi);
Decisions (document them): standard messages for non-numeric and out-of-range (parameterise bounds, standardise words); retry counts not tracked (add later if a lab needs them). This function graduates to your library the moment two programs agree on it.
R6 ★★ — The giant main (the rescue)
A 60-line main: reads 3 products (name-less: code, qty, unit price), computes per-product cost, applies a combined discount ladder, prints a table, prints totals. No functions. Produce: the extraction order, the function list, and what the narrator main looks like.
Target shape:
| order | extraction | why this order |
|---|---|---|
| 1 | double productCost(double price, int qty) |
purest, smallest |
| 2 | double applyLadder(double subtotal) |
pure decision |
| 3 | void printRow(...) / void printTotals(...) |
output once values stable |
| 4 | void readProduct(int& code, int& qty, double& price) |
reader (references!) |
| 5 | main narrates: for 3 products { read → cost → print } |
conductor |
main becomes ~8 lines. The Lab 6 rescue is the full-sized version with a real baseline table.
R7 ★★ — The flag-polluted policy
double finalPrice(double amount, bool student, bool member, bool wednesday) {
double p = amount;
if (student) p = p * 0.80;
if (member) p = p * 0.90;
if (wednesday) p = p * 0.95;
return p;
}
Works — but every caller passes three bools, and decisions Lab 3 taught you policies compose in order. Restructure so the ordering is visible and the flags shrink.
Target:
double applyPercentage(double price, double rate) { return price * rate; }
// caller pipeline, order visible in main:
// price = applyPercentage(price, 0.80); // student
// price = applyPercentage(price, 0.90); // member
One machine, called in documented order by the policy owner (main). Refactor honestly: byte-identical outputs on a baseline that covers all flag combinations (8 rows — you have the tooling now).
R8 ★★★ — The tangled validator
int main() {
// reads age; if invalid prints msg and EXITS
// reads marks; if any invalid prints msg and EXITS
// ... 40 lines mixing input, policy, output ...
}
The program exits-on-error deep inside its flow — three return 1s scattered through the reading code. Refactor so validation returns data instead of exiting: readers re-prompt (gallery patterns), main never inspects validity.
Target: readers per field (int readAge(), int readMark()) using the re-prompt gallery; policy functions pure; main = read → compute → print with zero decisions in it. The deep insight: exiting is a policy decision — it belongs to main, not to a reader. (Contrast with the guard-chain style from decisions Lab 4, which is honest at top level but doesn’t scale past two or three fields.)
R9 ★★★ — The copy-pasted slab ladder
The electricity-bill slab ladder now exists in three files: the decisions lab, the functions lesson walkthrough, and one of your own. Rates differ per file. Consolidate into one parameterised machine.
Target:
// slabs: first s1 units @ r1, next s2 @ r2, remainder @ r3
double slabBill(int units, int s1, double r1, int s2, double r2, double r3);
Six parameters — honest but noisy; document each. Test table: transfer each file’s old baseline rows into the one table (three boundary sets now owned by one function). Note the trade-off: parameterisation removes duplication but adds call-site noise — at three slab shapes you’d consider a struct (later units). Recognising that line is the ★★★ part.
R10 ★★★ — The untestable monolith
A program computes, prints, and reads inside the computation: applyLateFee asks the clerk “waived? y/n” mid-calculation. Design the split so applyLateFee becomes pure and the question moves to the caller, without changing any user-visible behaviour.
Target:
double applyLateFee(double fee, bool waived) { return waived ? 0 : fee * 1.10; }
// main:
// double fee = computeBase(...);
// bool waived = askYesNo("Waive late fee? (y/n): "); // a machine from gallery C
// fee = applyLateFee(fee, waived);
Baseline: run both versions on the same keystrokes — output identical, but the new one is testable in four check rows with no console interaction. The rule this exercise cements: input questions never live inside pure machines — the D9 lesson grown to include reading.
The one-page checklist (pin this)
- Baseline outputs on fixed inputs — written down.
- Extract one block per step; compile; run; compare; only then continue.
- Pure computations first, printers second, readers last.
- Name by single job; parameters in, results out (return or
&). - Duplicated code with different constants → parameterise, don’t fork.
- Globals → parameters/returns.
- Never mix “refactor” and “add feature” in one pass.