Refactoring Essentials
Quick reference guide for refactoring code effectively.
What is Refactoring?
Refactoring is improving code structure without changing its behavior:
> "Refactoring is a disciplined technique for restructuring existing code, altering its internal structure without changing its external behavior." - Martin Fowler
When to Refactor
The Rule of Three
Signs You Need to Refactor
Code Smells
Recognize patterns that indicate problems:
Bloaters
Long Method
// ❌ BAD: Method doing too much
function processOrder(order) {
// Validate order (20 lines)
if (!order.items) throw new Error('No items');
if (order.items.length === 0) throw new Error('Empty order');
// ... more validation
// Calculate totals (30 lines)
let subtotal = 0;
for (let item of order.items) {
subtotal += item.price * item.quantity;
}
// ... more calculation
// Apply discounts (25 lines)
let discount = 0;
if (order.couponCode) {
// ... discount logic
}
// Process payment (40 lines)
// ... payment logic
// Send notifications (20 lines)
// ... notification logic
}
// ✅ GOOD: Break into smaller methods
function processOrder(order) {
validateOrder(order);
const subtotal = calculateSubtotal(order);
const discount = applyDiscounts(order, subtotal);
const total = subtotal - discount;
processPayment(order, total);
sendOrderConfirmation(order);
}
Large Class
// ❌ BAD: Class doing too much
class User {
// User properties
name;
email;
// Database methods
save() {}
update() {}
delete() {}
// Authentication methods
login() {}
logout() {}
resetPassword() {}
// Email methods
sendWelcomeEmail() {}
sendPasswordResetEmail() {}
// Reporting methods
generateUserReport() {}
exportUserData() {}
}
// ✅ GOOD: Split into focused classes
class User {
name;
email;
}
class UserRepository {
save(user) {}
update(user) {}
delete(user) {}
}
class AuthenticationService {
login(user) {}
logout(user) {}
resetPassword(user) {}
}
class EmailService {
sendWelcomeEmail(user) {}
sendPasswordResetEmail(user) {}
}
class UserReportService {
generateReport(user) {}
exportData(user) {}
}
Long Parameter List
// ❌ BAD: Too many parameters
function createUser(
firstName,
lastName,
email,
phone,
address,
city,
country,
zipCode,
dateOfBirth
) {
// ...
}
// ✅ GOOD: Use parameter object
function createUser(userDetails) {
const {
firstName,
lastName,
email,
phone,
address,
city,
country,
zipCode,
dateOfBirth
} = userDetails;
// ...
}
// Or with TypeScript
interface UserDetails {
firstName: string;
lastName: string;
email: string;
phone: string;
address: string;
city: string;
country: string;
zipCode: string;
dateOfBirth: Date;
}
function createUser(details: UserDetails) {
// ...
}
Object-Orientation Abusers
Switch Statements
// ❌ BAD: Switch statement that will grow
function calculateShippingCost(order) {
switch (order.shippingMethod) {
case 'standard':
return order.weight * 5;
case 'express':
return order.weight * 10;
case 'overnight':
return order.weight * 20;
default:
throw new Error('Unknown shipping method');
}
}
// ✅ GOOD: Use polymorphism
class ShippingMethod {
calculateCost(order) {
throw new Error('Must be implemented by subclass');
}
}
class StandardShipping extends ShippingMethod {
calculateCost(order) {
return order.weight * 5;
}
}
class ExpressShipping extends ShippingMethod {
calculateCost(order) {
return order.weight * 10;
}
}
class OvernightShipping extends ShippingMethod {
calculateCost(order) {
return order.weight * 20;
}
}
// Or use strategy pattern with objects
const SHIPPING_STRATEGIES = {
standard: (order) => order.weight * 5,
express: (order) => order.weight * 10,
overnight: (order) => order.weight * 20,
};
function calculateShippingCost(order) {
const strategy = SHIPPING_STRATEGIES[order.shippingMethod];
if (!strategy) throw new Error('Unknown shipping method');
return strategy(order);
}
Change Preventers
Divergent Change
// ❌ BAD: Class changes for multiple reasons
class Order {
// Changes when database schema changes
saveToDB() {}
// Changes when calculation logic changes
calculateTotal() {}
// Changes when notification requirements change
sendEmail() {}
// Changes when reporting format changes
generateInvoice() {}
}
// ✅ GOOD: Separate concerns
class Order {
items;
total;
}
class OrderRepository {
save(order) {}
}
class OrderCalculator {
calculateTotal(order) {}
}
class OrderNotifier {
sendEmail(order) {}
}
class InvoiceGenerator {
generate(order) {}
}
Shotgun Surgery
// ❌ BAD: One change requires editing many classes
// Change commission rate requires editing:
// - SalesReport class
// - PaymentCalculator class
// - InvoiceGenerator class
// - DashboardWidget class
// - EmailTemplate class
// ✅ GOOD: Centralize related logic
class CommissionConfig {
static COMMISSION_RATE = 0.10;
static getRate() {
return this.COMMISSION_RATE;
}
static calculateCommission(amount) {
return amount * this.COMMISSION_RATE;
}
}
// All classes use CommissionConfig
class SalesReport {
generateReport(sales) {
const commission = CommissionConfig.calculateCommission(sales.total);
}
}
Dispensables
Duplicate Code
// ❌ BAD: Duplicated validation logic
function createUser(data) {
if (!data.email) throw new Error('Email required');
if (!/\S+@\S+\.\S+/.test(data.email)) throw new Error('Invalid email');
// ... create user
}
function updateUser(id, data) {
if (!data.email) throw new Error('Email required');
if (!/\S+@\S+\.\S+/.test(data.email)) throw new Error('Invalid email');
// ... update user
}
// ✅ GOOD: Extract common logic
function validateEmail(email) {
if (!email) throw new Error('Email required');
if (!/\S+@\S+\.\S+/.test(email)) throw new Error('Invalid email');
}
function createUser(data) {
validateEmail(data.email);
// ... create user
}
function updateUser(id, data) {
validateEmail(data.email);
// ... update user
}
Dead Code
// ❌ BAD: Commented out code
function processOrder(order) {
validateOrder(order);
// const discount = calculateDiscount(order);
// order.total = order.subtotal - discount;
calculateTotal(order);
processPayment(order);
}
// Old function no longer used
// function calculateDiscount(order) {
// return order.subtotal * 0.1;
// }
// ✅ GOOD: Remove dead code (use git to recover if needed)
function processOrder(order) {
validateOrder(order);
calculateTotal(order);
processPayment(order);
}
Speculative Generality
// ❌ BAD: Overly generic for no reason
class AbstractBaseUserFactoryProvider {
createUserFactoryStrategy() {
return new ConcreteUserFactory();
}
}
// ✅ GOOD: Keep it simple until generality is needed
class UserService {
createUser(data) {
return new User(data);
}
}
Refactoring Techniques
Extract Method
Break long methods into smaller, focused ones:
// ❌ BEFORE
function printOwing(invoice) {
console.log('***********************');
console.log('**** Customer Owes ****');
console.log('***********************');
let outstanding = 0;
for (const order of invoice.orders) {
outstanding += order.amount;
}
console.log(`Name: ${invoice.customer}`);
console.log(`Amount: ${outstanding}`);
}
// ✅ AFTER
function printOwing(invoice) {
printBanner();
const outstanding = calculateOutstanding(invoice);
printDetails(invoice, outstanding);
}
function printBanner() {
console.log('***********************');
console.log('**** Customer Owes ****');
console.log('***********************');
}
function calculateOutstanding(invoice) {
return invoice.orders.reduce((sum, order) => sum + order.amount, 0);
}
function printDetails(invoice, outstanding) {
console.log(`Name: ${invoice.customer}`);
console.log(`Amount: ${outstanding}`);
}
Inline Method
Replace unnecessary methods with their content:
// ❌ BEFORE: Unnecessary indirection
function getRating(driver) {
return moreThanFiveLateDeliveries(driver) ? 2 : 1;
}
function moreThanFiveLateDeliveries(driver) {
return driver.numberOfLateDeliveries > 5;
}
// ✅ AFTER
function getRating(driver) {
return driver.numberOfLateDeliveries > 5 ? 2 : 1;
}
Extract Variable
Make complex expressions readable:
// ❌ BEFORE
function price(order) {
return order.quantity * order.itemPrice -
Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
Math.min(order.quantity * order.itemPrice * 0.1, 100);
}
// ✅ AFTER
function price(order) {
const basePrice = order.quantity * order.itemPrice;
const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
const shipping = Math.min(basePrice * 0.1, 100);
return basePrice - quantityDiscount + shipping;
}
Replace Temp with Query
Replace temporary variables with method calls:
// ❌ BEFORE
function getPrice() {
const basePrice = quantity * itemPrice;
const discountFactor = basePrice > 1000 ? 0.95 : 0.98;
return basePrice * discountFactor;
}
// ✅ AFTER
function getPrice() {
return basePrice() * discountFactor();
}
function basePrice() {
return quantity * itemPrice;
}
function discountFactor() {
return basePrice() > 1000 ? 0.95 : 0.98;
}
Replace Conditional with Polymorphism
Use inheritance instead of conditionals:
// ❌ BEFORE
class Bird {
getSpeed() {
switch (this.type) {
case 'European':
return this.baseSpeed;
case 'African':
return this.baseSpeed - this.loadFactor;
case 'Norwegian':
return this.isNailed ? 0 : this.baseSpeed;
}
}
}
// ✅ AFTER
class Bird {
getSpeed() {
throw new Error('Must be implemented by subclass');
}
}
class EuropeanBird extends Bird {
getSpeed() {
return this.baseSpeed;
}
}
class AfricanBird extends Bird {
getSpeed() {
return this.baseSpeed - this.loadFactor;
}
}
class NorwegianBird extends Bird {
getSpeed() {
return this.isNailed ? 0 : this.baseSpeed;
}
}
Introduce Parameter Object
Group related parameters:
// ❌ BEFORE
function amountInvoiced(startDate, endDate) {}
function amountReceived(startDate, endDate) {}
function amountOverdue(startDate, endDate) {}
// ✅ AFTER
class DateRange {
constructor(startDate, endDate) {
this.startDate = startDate;
this.endDate = endDate;
}
}
function amountInvoiced(dateRange) {}
function amountReceived(dateRange) {}
function amountOverdue(dateRange) {}
Replace Magic Number with Constant
Give numbers meaningful names:
// ❌ BEFORE
function potentialEnergy(mass, height) {
return mass * height * 9.81;
}
// ✅ AFTER
const GRAVITATIONAL_CONSTANT = 9.81;
function potentialEnergy(mass, height) {
return mass * height * GRAVITATIONAL_CONSTANT;
}
Refactoring Patterns
Guard Clauses
Replace nested conditionals with early returns:
// ❌ BEFORE
function getPayAmount(employee) {
let result;
if (employee.isSeparated) {
result = { amount: 0, reasonCode: 'SEP' };
} else {
if (employee.isRetired) {
result = { amount: 0, reasonCode: 'RET' };
} else {
// ... calculate normal pay
result = { amount: normalPay, reasonCode: 'NORM' };
}
}
return result;
}
// ✅ AFTER
function getPayAmount(employee) {
if (employee.isSeparated) {
return { amount: 0, reasonCode: 'SEP' };
}
if (employee.isRetired) {
return { amount: 0, reasonCode: 'RET' };
}
// ... calculate normal pay
return { amount: normalPay, reasonCode: 'NORM' };
}
Consolidate Conditional Expression
Combine related conditions:
// ❌ BEFORE
function disabilityAmount(employee) {
if (employee.seniority < 2) return 0;
if (employee.monthsDisabled > 12) return 0;
if (employee.isPartTime) return 0;
// ... calculate disability
}
// ✅ AFTER
function disabilityAmount(employee) {
if (isNotEligibleForDisability(employee)) return 0;
// ... calculate disability
}
function isNotEligibleForDisability(employee) {
return employee.seniority < 2
|| employee.monthsDisabled > 12
|| employee.isPartTime;
}
Decompose Conditional
Extract conditions and actions into methods:
// ❌ BEFORE
if (date.before(SUMMER_START) || date.after(SUMMER_END)) {
charge = quantity * winterRate + winterServiceCharge;
} else {
charge = quantity * summerRate;
}
// ✅ AFTER
if (isWinter(date)) {
charge = winterCharge(quantity);
} else {
charge = summerCharge(quantity);
}
function isWinter(date) {
return date.before(SUMMER_START) || date.after(SUMMER_END);
}
function winterCharge(quantity) {
return quantity * winterRate + winterServiceCharge;
}
function summerCharge(quantity) {
return quantity * summerRate;
}
Safe Refactoring Process
Step-by-Step Approach
Red-Green-Refactor (TDD)
// 1. RED: Write failing test
test('calculates discount for bulk orders', () => {
const order = { quantity: 100, itemPrice: 10 };
expect(calculateDiscount(order)).toBe(50);
});
// 2. GREEN: Make it pass (quick and dirty)
function calculateDiscount(order) {
if (order.quantity > 50) {
return order.quantity * order.itemPrice * 0.05;
}
return 0;
}
// 3. REFACTOR: Clean up code
function calculateDiscount(order) {
const BULK_THRESHOLD = 50;
const BULK_DISCOUNT_RATE = 0.05;
if (!isBulkOrder(order)) return 0;
return basePrice(order) * BULK_DISCOUNT_RATE;
}
function isBulkOrder(order) {
return order.quantity > BULK_THRESHOLD;
}
function basePrice(order) {
return order.quantity * order.itemPrice;
}