Safe Refactoring Techniques
A teammate once "refactored" a payment module and broke production. The code looked cleaner. The tests passed. But a subtle behavior change—rounding in a different direction—cost the company thousands in incorrect charges.
Refactoring means changing structure without changing behavior. If behavior changes, it's not refactoring. It's a feature or a bug.
The techniques in this lesson are your safety toolkit. Small, reversible steps. One at a time.
The Golden Rule: One Refactoring at a Time
Never mix refactoring with feature work. Never do five refactorings in one commit.
Why? When something breaks, you need to know which change caused it. One refactoring = one commit = easy rollback.
// BAD: Extracting, renaming, and inlining in one go
// GOOD: Extract method. Commit. Rename. Commit. Inline. Commit.
Extract Method / Function
The most powerful refactoring. Take a block of code that does one thing and give it a name.
Before
function processOrder(order) {
// Validate
if (!order.items?.length) throw new Error('Empty order');
if (!order.customer?.email) throw new Error('Missing email');
if (order.total < 0) throw new Error('Invalid total');
// Calculate
const subtotal = order.items.reduce((sum, i) => sum + i.price * i.quantity, 0);
const tax = subtotal * 0.08;
const shipping = order.items.length > 5 ? 15 : 8;
// Process
chargePayment(order.paymentMethod, subtotal + tax + shipping);
sendConfirmation(order.customer.email, order);
updateInventory(order.items);
}
After
function processOrder(order) {
validateOrder(order);
const total = calculateOrderTotal(order);
chargeAndFulfill(order, total);
}
function validateOrder(order) {
if (!order.items?.length) throw new Error('Empty order');
if (!order.customer?.email) throw new Error('Missing email');
if (order.total < 0) throw new Error('Invalid total');
}
function calculateOrderTotal(order) {
const subtotal = order.items.reduce((sum, i) => sum + i.price * i.quantity, 0);
const tax = subtotal * 0.08;
const shipping = order.items.length > 5 ? 15 : 8;
return subtotal + tax + shipping;
}
function chargeAndFulfill(order, total) {
chargePayment(order.paymentMethod, total);
sendConfirmation(order.customer.email, order);
updateInventory(order.items);
}
Each extracted function has a single responsibility. The main function reads like a table of contents.
When to extract: When a block of code has a clear purpose you can name. When you'd add a comment to explain it—extract instead.
Extract Variable
Give a complex expression a name. Improves readability and often reveals intent.
Before
if (user.age >= 18 && user.verified && !user.suspended && user.subscription?.status === 'active') {
showPremiumContent();
}
After
const isEligibleForPremium =
user.age >= 18 &&
user.verified &&
!user.suspended &&
user.subscription?.status === 'active';
if (isEligibleForPremium) {
showPremiumContent();
}
The variable name documents the business rule. The condition becomes self-explanatory.
In TypeScript, extracted variables also help type narrowing:
const order = getOrder(id);
if (order && order.status === 'shipped' && order.trackingNumber) {
// TypeScript knows order has trackingNumber here
displayTracking(order.trackingNumber);
}
Rename
Low risk. High value. Do it often.
Poor names are the most common code smell. Renaming is the cheapest fix.
// Before: What does "d" mean?
function calc(d) {
return d * 1.1;
}
// After: Intent is clear
function calculatePriceWithTax(priceBeforeTax) {
return priceBeforeTax * 1.1;
}
Use your IDE. Modern editors (VS Code, WebStorm) rename across the entire codebase safely. Don't do it manually—you'll miss references.
Rename in small steps: If a variable is used in 20 places, rename it. The IDE updates all 20. One commit.
Inline
The opposite of extract. Remove indirection when it adds no value.
Sometimes we over-extract. A one-line function that's only called once might not earn its name.
Before
function getFullName(user) {
return `${user.firstName} ${user.lastName}`;
}
function displayUser(user) {
const name = getFullName(user);
return `<h1>${name}</h1>`;
}
After (when used once)
function displayUser(user) {
return `<h1>${user.firstName} ${user.lastName}</h1>`;
}
When to inline: When the extracted function is trivial, used once, and the name doesn't add clarity. When a wrapper exists "for flexibility" but nobody ever needs that flexibility.
When not to inline: When the extracted name carries important domain meaning. When the logic is reused. When inlining would make the caller harder to read.
Move Method
Put the method on the class that has the data it needs.
Feature Envy (from the previous lesson) is fixed by moving the method.
Before
class Order {
constructor(public items: LineItem[], public customer: Customer) {}
}
class InvoiceGenerator {
generate(order: Order): string {
// This method uses order's data more than its own
const subtotal = order.items.reduce((sum, i) => sum + i.price * i.quantity, 0);
const tax = subtotal * 0.08;
return `Total: $${(subtotal + tax).toFixed(2)}`;
}
}
After
class Order {
constructor(public items: LineItem[], public customer: Customer) {}
calculateTotal(): number {
const subtotal = this.items.reduce((sum, i) => sum + i.price * i.quantity, 0);
const tax = subtotal * 0.08;
return subtotal + tax;
}
}
class InvoiceGenerator {
generate(order: Order): string {
return `Total: $${order.calculateTotal().toFixed(2)}`;
}
}
The calculation lives with the data. InvoiceGenerator orchestrates; Order owns its totals.
IDE Refactoring Tools
Your IDE does the mechanical work. Use it.
| Refactoring | VS Code / Cursor | WebStorm |
|---|---|---|
| Extract function | Select → Refactor → Extract | Ctrl+Alt+M |
| Extract variable | Select → Refactor → Extract | Ctrl+Alt+V |
| Rename symbol | F2 | Shift+F6 |
| Inline | Right-click → Inline | Ctrl+Alt+N |
Why use the IDE: It updates all references. It handles edge cases (imports, re-exports, type definitions). It's faster and safer than manual editing.
When the IDE fails: Complex cases—dynamic property access, reflection, string-based lookups. Then refactor manually, but still one step at a time.
The Refactoring Workflow
- Ensure tests pass before you start.
- Make one small change. Extract one method. Rename one variable.
- Run tests. If they fail, you broke something. Fix it before continuing.
- Commit. Small, atomic commits. "Extract validateOrder" is a good commit message.
- Repeat.
This loop—change, test, commit—is the safety net. It makes refactoring predictable.
When Refactoring Feels Risky
If you're nervous, the code probably lacks tests. That's a different problem (covered in the next lesson).
If you have tests and you're still nervous:
- Make the change smaller
- Commit more frequently
- Use the IDE's refactoring tools—they're battle-tested
Key insight: Refactoring = structure changes, behavior stays the same. One refactoring at a time. Extract, rename, inline, move. Use your IDE. Test after each step. Commit often.