Skip to main content

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

  1. State the mechanics of Extract Function from memory (at least 5 steps).
  2. What does Fowler say is the argument for Extract Function that won over "reuse" and "screen size"?
  3. Why is Change Function Declaration often done in two commits?
  4. What signal suggests Inline Function rather than Extract?
  5. 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

  1. "I extracted a function called helper." - what is wrong?
  2. "I did Extract Function on a 20-line fragment that assigns three different local variables." - what should come first?
  3. "I renamed a public API method using the IDE refactor." - when is this fine, when not?
  4. "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":

  1. Introduce a seam if none exists.
  2. Apply Change Function Declaration to rename to schedule(date, frequency). Use the parallel-change two-commit form.
  3. 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