Refactoring Techniques: Improving Code Without Changing Behavior
Refactoring changes code structure without changing external behavior. Always refactor with tests in place.
Extract Function
// Before
function printOwing(invoice: Invoice): void {
let outstanding = 0;
console.log('***********************');
console.log('**** Customer Owes ****');
console.log('***********************');
for (const order of invoice.orders) {
outstanding += order.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}`);
}
// After
function printOwing(invoice: Invoice): void {
printBanner();
const outstanding = calculateOutstanding(invoice);
recordDueDate(invoice);
printDetails(invoice, outstanding);
}
function printBanner(): void {
console.log('***********************');
console.log('**** Customer Owes ****');
console.log('***********************');
}
function calculateOutstanding(invoice: Invoice): number {
return invoice.orders.reduce((sum, order) => sum + order.amount, 0);
}
function recordDueDate(invoice: Invoice): void {
const today = new Date();
invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
}
Introduce Parameter Object
// Before: Multiple related parameters
function createDateRange(startYear: number, startMonth: number, startDay: number,
endYear: number, endMonth: number, endDay: number): void { }
// After: Group into an object
interface DateRange {
start: Date;
end: Date;
}
function createDateRange(range: DateRange): void { }
Replace Conditional with Polymorphism
// Before: Switch statement on type
function getSpeed(animal: Animal): number {
switch (animal.type) {
case 'bird': return animal.airSpeed;
case 'horse': return animal.runSpeed;
case 'fish': return animal.swimSpeed;
default: throw new Error('Unknown animal');
}
}
// After: Polymorphism
abstract class Animal {
abstract getSpeed(): number;
}
class Bird extends Animal {
getSpeed(): number { return this.airSpeed; }
}
class Horse extends Animal {
getSpeed(): number { return this.runSpeed; }
}
class Fish extends Animal {
getSpeed(): number { return this.swimSpeed; }
}
Replace Magic Numbers with Named Constants
// Before
function calculateDiscount(price: number, days: number): number {
if (days > 90) return price * 0.15;
if (days > 30) return price * 0.1;
return 0;
}
// After
const SENIOR_CUSTOMER_DAYS = 90;
const REGULAR_CUSTOMER_DAYS = 30;
const SENIOR_DISCOUNT_RATE = 0.15;
const REGULAR_DISCOUNT_RATE = 0.1;
function calculateDiscount(price: number, daysSinceRegistration: number): number {
if (daysSinceRegistration > SENIOR_CUSTOMER_DAYS) return price * SENIOR_DISCOUNT_RATE;
if (daysSinceRegistration > REGULAR_CUSTOMER_DAYS) return price * REGULAR_DISCOUNT_RATE;
return 0;
}
Guard Clauses
// Before: Nested conditions
function processPayment(payment: Payment): void {
if (payment !== null) {
if (payment.amount > 0) {
if (!payment.isExpired()) {
// actual processing logic buried here
chargeCard(payment);
}
}
}
}
// After: Early returns / guard clauses
function processPayment(payment: Payment): void {
if (!payment) return;
if (payment.amount <= 0) return;
if (payment.isExpired()) throw new Error('Payment method expired');
chargeCard(payment);
}
Move Field / Extract Class
// Before: Person has too many phone-related fields
class Person {
name: string;
officePhoneNumber: string;
officeAreaCode: string;
homePhoneNumber: string;
homeAreaCode: string;
}
// After: Extract TelephoneNumber class
class TelephoneNumber {
constructor(
readonly areaCode: string,
readonly number: string
) {}
toString(): string { return `(${this.areaCode}) ${this.number}`; }
}
class Person {
name: string;
officePhone: TelephoneNumber;
homePhone: TelephoneNumber;
}
The key to successful refactoring is doing it in small, safe steps backed by a solid test suite.