Code Katas
Seven refactor katas with concrete before/after code. Complete each at least twice. The first time is for learning the move; the second is for fluency. Tests must stay green between every named step.
Kata 1: Extract Till You Drop
Time limit: 15 minutes Goal: Extract Function until the top-level function reads as intent, not implementation. Setup: Start with this 20-line function (replicate the original tests -- one snapshot is enough for fluency work).
function statement(invoice, plays) {
let totalAmount = 0;
let volumeCredits = 0;
let result = `Statement for ${invoice.customer}\n`;
const format = new Intl.NumberFormat("en-US",
{ style: "currency", currency: "USD", minimumFractionDigits: 2 }).format;
for (const perf of invoice.performances) {
const play = plays[perf.playID];
let thisAmount = 0;
switch (play.type) {
case "tragedy":
thisAmount = 40000;
if (perf.audience > 30) thisAmount += 1000 * (perf.audience - 30);
break;
case "comedy":
thisAmount = 30000;
if (perf.audience > 20) thisAmount += 10000 + 500 * (perf.audience - 20);
thisAmount += 300 * perf.audience;
break;
default: throw new Error(`unknown type: ${play.type}`);
}
volumeCredits += Math.max(perf.audience - 30, 0);
if ("comedy" === play.type) volumeCredits += Math.floor(perf.audience / 5);
result += ` ${play.name}: ${format(thisAmount / 100)} (${perf.audience} seats)\n`;
totalAmount += thisAmount;
}
result += `Amount owed is ${format(totalAmount / 100)}\n`;
result += `You earned ${volumeCredits} credits\n`;
return result;
}
Target: produce a top-level function of ≤ 6 lines using named helpers (amountFor, volumeCreditsFor, playFor, usd, totalAmount, totalVolumeCredits).
Repeat until: you can do the full extraction in under 10 minutes with all tests green at every step.
Kata 2: Rename for Clarity
Time limit: 15 minutes Goal: Rename poorly-named variables and one function without breaking tests. Setup:
function f(d, r) {
let x = 0;
for (const i of d) {
if (i.t === "p") x += i.a;
}
return x * r;
}
Target: produce totalPaid(orders, rate) with paid as the filter predicate's intent name. If callers exist, use the two-commit parallel-change form for the outer rename.
Repeat until: you rename without backtracking and without looking up the IDE shortcut.
Kata 3: Split Phase
Time limit: 20 minutes Goal: Separate parsing from pricing. Setup:
function priceFromString(orderString, priceList) {
const parts = orderString.split(/\s+/);
const sku = parts[0].split("-")[1];
const qty = parseInt(parts[1]);
return qty * priceList[sku];
}
Target: two functions, parseOrder(s) returning {sku, qty} and price(order, priceList). Each must be unit-testable in isolation (parsing without a price list, pricing with a hand-made record).
Repeat until: you naturally reach for Split Phase whenever you see "get data in shape, then compute."
Kata 4: Replace Primitive with Object
Time limit: 20 minutes Goal: Wrap a scattered string field. Setup:
const orders = [...];
const hot = orders.filter(o => o.priority === "high" || o.priority === "rush");
const byPriorityDesc = [...orders].sort((a, b) =>
["low","normal","high","rush"].indexOf(b.priority) -
["low","normal","high","rush"].indexOf(a.priority)
);
Target: a Priority class; o.priority.higherThan(...) replaces the comparisons; the rank table lives once in the class.
Repeat until: you can identify a primitive-with-behavior candidate in production code within 60 seconds of reading it.
Kata 5: Replace Conditional with Polymorphism
Time limit: 25 minutes
Goal: Turn two recurring switches into one class hierarchy + factory.
Setup: a bird simulation where both plumage(bird) and airSpeedVelocity(bird) switch on bird.type.
function plumage(bird) {
switch (bird.type) {
case "EuropeanSwallow": return "average";
case "AfricanSwallow": return bird.numberOfCoconuts > 2 ? "tired" : "average";
case "NorwegianBlueParrot":return bird.voltage > 100 ? "scorched" : "beautiful";
default: return "unknown";
}
}
function airSpeedVelocity(bird) {
switch (bird.type) {
case "EuropeanSwallow": return 35;
case "AfricanSwallow": return 40 - 2 * bird.numberOfCoconuts;
case "NorwegianBlueParrot":return bird.isNailed ? 0 : 10 + bird.voltage / 10;
default: return null;
}
}
Target: Bird superclass, three subclasses, a birdFor(data) factory. Both plumage and airSpeedVelocity disappear from the top level; call sites use bird.plumage and bird.airSpeedVelocity.
Repeat until: you can defend why polymorphism pays here (recurrence across both functions) vs when it would not (if only plumage existed).
Kata 6: Introduce Parameter Object
Time limit: 15 minutes
Goal: Collapse a data clump.
Setup: three functions with the same (customer, startDate, endDate) signature.
Target: a DateRange class with an includes(date) method, used by at least two of the three functions in the refactored form.
Repeat until: you notice clumps in meeting notes, not only in code.
Kata 7: Parallel Change
Time limit: 25 minutes
Goal: Change a published function signature without breaking any caller, across two commits.
Setup: function circum(radius) { ... } used in 12 places across the codebase (simulate with a grep target).
Target (commit 1, expand): add function circumference(radius) { ... } and make circum delegate to it. No caller is touched. All tests green.
Target (commit 2, migrate + contract): migrate all callers to circumference. Delete circum. All tests green.
Repeat until: you can execute expand/migrate/contract without accidentally combining steps into one commit.
Completion Standard
- Each kata completed twice
- You can state out loud which move you are applying before every edit
- Every test suite runs between every named step
- At least one of Katas 5, 6, or 7 was done on real code you work with, not the provided sample