Small Refactor Moves Workshop
This workshop drills the five highest-frequency moves: Extract Function, Inline Function, Extract Variable, Rename, Change Function Declaration, Move Function. The goal is speed with green tests, not speed alone.
Retrieval Prompts
- State the mechanics of Extract Function from memory (at least 5 steps).
- What does Fowler say is the argument for Extract Function that won over "reuse" and "screen size"?
- Why is Change Function Declaration often done in two commits?
- What signal suggests Inline Function rather than Extract?
- What is the difference between Move Function and Change Function Declaration?
Compare and Distinguish
- Extract Variable vs Extract Function (when does each win?)
- Rename Variable vs Change Function Declaration (local vs published)
- Move Function vs Move Field (what smell is each fixing?)
- Inline Function vs Inline Variable (when would you apply one but not the other?)
Common Mistake Check
- "I extracted a function called
helper." - what is wrong? - "I did Extract Function on a 20-line fragment that assigns three different local variables." - what should come first?
- "I renamed a public API method using the IDE refactor." - when is this fine, when not?
- "I moved the function and deleted the old call site in the same commit." - how could this burn you?
Mini Application
Drill 1: Extract Function Chain (15 minutes)
Starting code (from Fowler):
function printOwing(invoice) {
let outstanding = 0;
console.log("***********************");
console.log("**** Customer Owes ****");
console.log("***********************");
for (const o of invoice.orders) outstanding += o.amount;
const today = new Date();
invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
console.log(`name: ${invoice.customer}`);
console.log(`amount: ${outstanding}`);
console.log(`due: ${invoice.dueDate.toLocaleDateString()}`);
}
Write one test that pins current output (capture console via spy). Then do three Extract Function moves to yield a top-level function that reads like printBanner(); outstanding = calculateOutstanding(invoice); recordDueDate(invoice, clock); printDetails(invoice, outstanding);. Run the test after every step.
Drill 2: Rename Across a Boundary (10 minutes)
Given a function f(d, t) used in 12 call sites with d being a Date and t being "daily" | "monthly":
- Introduce a seam if none exists.
- Apply Change Function Declaration to rename to
schedule(date, frequency). Use the parallel-change two-commit form. - Verify that the first commit does not change any caller; only adds a delegating new name. Second commit migrates call sites.
Drill 3: Move Function from Feature Envy (15 minutes)
Given:
class Order {
overdraftChargeFor(type, daysOverdrawn) {
if (type.isPremium) { return /* complex expression using type.* */ }
else { return /* different expression using type.* */ }
}
}
The function uses type.* much more than this.*. Apply Move Function to AccountType. Follow the delegating-wrapper mechanics. Run the test suite after each step. Remove the delegating wrapper in a separate commit.
Drill 4: Inline What Does Not Earn Its Name (10 minutes)
Given:
function rating(aDriver) { return moreThanFiveLateDeliveries(aDriver) ? 2 : 1; }
function moreThanFiveLateDeliveries(d) { return d.numberOfLateDeliveries > 5; }
If the latter is not reused and the predicate has exactly the same information content as driver.numberOfLateDeliveries > 5, apply Inline Function. Verify tests are still green.
Evidence Check
This workshop is complete only if:
- Drill 1 produced a 4-line top-level function with every character of output identical to the original
- Drill 2 was committed as two separate commits, one pure-refactor
- Drill 3's move was completed and the delegating wrapper removed
- Every drill's test suite was run between every named step
- You can state out loud which of the five moves each step was