Refactoring Toward Better Design
Common Refactoring Patterns
Read aloud is unavailable in this browser.
The Refactoring Catalog: Tools for Transformation
Refactoring patterns are named, repeatable transformations that improve code structure while preserving behavior. Having a shared vocabulary of these patterns enables precise communication during code reviews, pair programming sessions, and LLD interviews.
Martin Fowler's Refactoring catalog documents over 60 patterns, but most real-world refactoring draws from a smaller set of frequently used transformations. This page covers the essential patterns that address the most common code smells and structural problems.
Each pattern follows a predictable structure:
- Motivation: Why and when to apply this pattern
- Mechanics: Step-by-step transformation process
- Example: Before and after code demonstrating the pattern
- Considerations: Trade-offs and related patterns
By the end of this page, you will have internalized the most important refactoring patterns, understand their mechanical application, and be able to identify which pattern addresses which code smell. This knowledge is directly applicable in LLD interviews where demonstrating clean code practices carries significant weight.
Extract Method
Extract Method is the single most important and frequently used refactoring pattern. It takes a code fragment and transforms it into a method whose name explains its purpose.
Motivation:
- Long methods that are difficult to understand
- Duplicated code fragments across methods
- Code blocks with clear conceptual purpose
- Need to make code more testable
The fundamental principle: A method should do one thing at one level of abstraction. When you can name a code fragment, you should extract it into a method with that name.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
// BEFORE: Long method with mixed abstraction levelsclass OrderProcessor { processOrder(order: Order): void { // Validate order if (!order.items || order.items.length === 0) { throw new Error('Order must have at least one item'); } if (!order.customer || !order.customer.id) { throw new Error('Order must have a valid customer'); } for (const item of order.items) { if (!item.productId || item.quantity <= 0) { throw new Error('Invalid order item'); } } // Calculate total let subtotal = 0; for (const item of order.items) { subtotal += item.price * item.quantity; } const tax = subtotal * 0.08; const shipping = subtotal > 100 ? 0 : 10; const total = subtotal + tax + shipping; // Create invoice const invoice = { orderId: order.id, customerId: order.customer.id, items: order.items, subtotal, tax, shipping, total, createdAt: new Date() }; this.invoiceRepository.save(invoice); this.emailService.sendInvoice(order.customer.email, invoice); }} // AFTER: Extracted methods with clear responsibilitiesclass OrderProcessor { processOrder(order: Order): void { this.validateOrder(order); const totals = this.calculateTotals(order); const invoice = this.createInvoice(order, totals); this.deliverInvoice(order.customer, invoice); } private validateOrder(order: Order): void { this.validateOrderHasItems(order); this.validateOrderHasCustomer(order); this.validateOrderItems(order.items); } private validateOrderHasItems(order: Order): void { if (!order.items || order.items.length === 0) { throw new Error('Order must have at least one item'); } } private validateOrderHasCustomer(order: Order): void { if (!order.customer || !order.customer.id) { throw new Error('Order must have a valid customer'); } } private validateOrderItems(items: OrderItem[]): void { for (const item of items) { if (!item.productId || item.quantity <= 0) { throw new Error('Invalid order item'); } } } private calculateTotals(order: Order): OrderTotals { const subtotal = this.calculateSubtotal(order.items); const tax = this.calculateTax(subtotal); const shipping = this.calculateShipping(subtotal); return { subtotal, tax, shipping, total: subtotal + tax + shipping }; } private calculateSubtotal(items: OrderItem[]): number { return items.reduce((sum, item) => sum + item.price * item.quantity, 0); } private calculateTax(subtotal: number): number { return subtotal * this.TAX_RATE; } private calculateShipping(subtotal: number): number { return subtotal > this.FREE_SHIPPING_THRESHOLD ? 0 : this.SHIPPING_COST; } private createInvoice(order: Order, totals: OrderTotals): Invoice { return { orderId: order.id, customerId: order.customer.id, items: order.items, ...totals, createdAt: new Date() }; } private deliverInvoice(customer: Customer, invoice: Invoice): void { this.invoiceRepository.save(invoice); this.emailService.sendInvoice(customer.email, invoice); }}- Create a new method with an intention-revealing name. 2. Copy the extracted code into the new method. 3. Identify local variables used in the extracted code. 4. Determine if any variables are modified—these may become return values. 5. Compile to check for scope issues. 6. Replace the original code with a call to the new method. 7. Test to ensure behavior is preserved.
Extract Class
Extract Class creates a new class by moving a subset of fields and methods from an existing class. This is the primary remedy for the Large Class smell and violations of the Single Responsibility Principle.
Motivation:
- A class has grown too large to understand easily
- A subset of fields and methods form a cohesive group
- The same data is always used together
- Multiple responsibilities have accumulated in one class
Signs you need Extract Class:
- Methods that only use a subset of the class's fields
- Groups of fields that have similar prefixes (e.g.,
billingAddress,billingCity,billingZip) - Data clumps that appear together across methods
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
// BEFORE: Class with multiple responsibilitiesclass Employee { private id: string; private name: string; private email: string; private department: string; // Address-related fields - cohesive group private street: string; private city: string; private state: string; private zipCode: string; private country: string; // Phone-related fields - another cohesive group private areaCode: string; private phoneNumber: string; private extension: string; getFullAddress(): string { return `${this.street}${this.city}, ${this.state} ${this.zipCode}${this.country}`; } getFormattedPhone(): string { let result = `(${this.areaCode}) ${this.phoneNumber}`; if (this.extension) { result += ` x${this.extension}`; } return result; } validateAddress(): boolean { return this.street?.length > 0 && this.city?.length > 0 && this.state?.length === 2 && this.zipCode?.match(/^\d{5}(-\d{4})?$/) !== null; } // ... many more methods mixing employee logic with address/phone logic} // AFTER: Cohesive classes with single responsibilitiesclass Address { constructor( private street: string, private city: string, private state: string, private zipCode: string, private country: string ) {} getFullAddress(): string { return `${this.street}${this.city}, ${this.state} ${this.zipCode}${this.country}`; } validate(): boolean { return this.street?.length > 0 && this.city?.length > 0 && this.state?.length === 2 && this.zipCode?.match(/^\d{5}(-\d{4})?$/) !== null; } // Address can now evolve independently getMailingLabel(): string { /* ... */ } distanceTo(other: Address): number { /* ... */ }} class PhoneNumber { constructor( private areaCode: string, private number: string, private extension?: string ) {} format(): string { let result = `(${this.areaCode}) ${this.number}`; if (this.extension) { result += ` x${this.extension}`; } return result; } validate(): boolean { return this.areaCode?.match(/^\d{3}$/) !== null && this.number?.match(/^\d{3}-\d{4}$/) !== null; } // PhoneNumber can now evolve independently isInternational(): boolean { /* ... */ }} class Employee { constructor( private id: string, private name: string, private email: string, private department: string, private address: Address, private phone: PhoneNumber ) {} // Employee is now focused on employee-specific behavior getContactInfo(): string { return `${this.name}${this.address.getFullAddress()}${this.phone.format()}`; } isContactValid(): boolean { return this.address.validate() && this.phone.validate(); }}Look for fields that change together, methods that use the same subset of fields, and data that has meaning independent of the original class. Address and PhoneNumber are meaningful concepts on their own—they don't require Employee to make sense. This semantic independence signals extraction opportunity.
Move Method and Move Field
Move Method and Move Field relocate functionality to the class where it belongs. These patterns address Feature Envy—when a method uses more data from another class than from its own.
Motivation:
- A method repeatedly accesses data from another object
- A field is used more by other classes than by its own
- Related functionality is scattered across classes
- Changes to one concept require changes in multiple classes
The guiding principle: Things that change for the same reason should be together. Things that change for different reasons should be apart.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
// BEFORE: Method envies data from another classclass Account { private balance: number; private overdraftLimit: number; private accountType: AccountType; getBalance(): number { return this.balance; } getOverdraftLimit(): number { return this.overdraftLimit; } getAccountType(): AccountType { return this.accountType; }} class BankReportService { // This method uses Account data more than its own calculateOverdraftFee(account: Account, amount: number): number { if (account.getAccountType() === AccountType.PREMIUM) { return 0; // Premium accounts have no overdraft fees } const overdraftAmount = amount - account.getBalance(); if (overdraftAmount <= 0) { return 0; // Not actually overdrawing } if (overdraftAmount > account.getOverdraftLimit()) { return 35.00; // Over limit: maximum fee } return overdraftAmount * 0.05; // 5% of overdraft amount }} // AFTER: Method moved to where the data livesclass Account { private balance: number; private overdraftLimit: number; private accountType: AccountType; // Method now lives with its data calculateOverdraftFee(withdrawalAmount: number): number { if (this.accountType === AccountType.PREMIUM) { return 0; } const overdraftAmount = withdrawalAmount - this.balance; if (overdraftAmount <= 0) { return 0; } if (overdraftAmount > this.overdraftLimit) { return Account.MAXIMUM_OVERDRAFT_FEE; } return overdraftAmount * Account.OVERDRAFT_FEE_RATE; } private static readonly MAXIMUM_OVERDRAFT_FEE = 35.00; private static readonly OVERDRAFT_FEE_RATE = 0.05;} class BankReportService { // Caller becomes simpler, delegating to Account generateOverdraftReport(account: Account, amount: number): Report { const fee = account.calculateOverdraftFee(amount); // ... reporting logic that is actually this service's concern }}Move Field mechanics:
Move Field follows similar logic but for data rather than behavior:
- If the field is public, encapsulate it first (use getters/setters)
- Create the field in the target class with appropriate access
- Add a reference from the source class to the target class (if not already present)
- Change all references in the source class to use the target class's field
- Remove the field from the source class
- Compile and test
Fields should live where their methods live. If you've moved methods to a class, the fields they use often should follow.
Move Method often transforms code from 'Ask' style (querying data then computing) to 'Tell' style (telling an object to do something). The latter typically results in better encapsulation and clearer responsibilities.
Rename Method, Variable, and Class
Renaming is often underestimated as a refactoring technique, but it's one of the most powerful. Good names reveal intent, reduce the need for comments, and make code self-documenting.
Motivation:
- Names no longer reflect what code does (after evolution)
- Names are too vague, cryptic, or misleading
- Names use inconsistent conventions
- Names expose implementation instead of intent
The core principle: Code is read far more often than it's written. Investing in good names pays dividends on every future read.
| Before | After | Why Better |
|---|---|---|
data | userPreferences | Reveals what data represents |
process() | validateAndEnrichOrder() | Describes what processing does |
flag | isEligibleForDiscount | Boolean reads naturally in conditionals |
tmp | remainingBalance | Domain term instead of implementation detail |
Manager | OrderRepository | Specific responsibility instead of vague role |
doIt() | submitPayment() | Action is clear from name |
list | pendingOrders | Contents and status are visible |
x, y | latitude, longitude | Domain semantics, not arbitrary labels |
123456789101112131415161718192021222324252627282930313233343536373839
// BEFORE: Cryptic names require mental translationfunction calc(a: number[], b: number): number { let r = 0; for (let i = 0; i < a.length; i++) { if (a[i] > b) { r += a[i]; } } return r;} // AFTER: Names reveal intentfunction sumValuesAboveThreshold(values: number[], threshold: number): number { let total = 0; for (const value of values) { if (value > threshold) { total += value; } } return total;} // EVEN BETTER: Using modern constructs that name the patternfunction sumValuesAboveThreshold(values: number[], threshold: number): number { return values .filter(value => value > threshold) .reduce((sum, value) => sum + value, 0);} // CLASS RENAMING EXAMPLE// BEFORE: What does this class do?class Manager { handle(input: unknown): unknown { /* ... */ }} // AFTER: Clear responsibilityclass OrderValidationService { validateOrder(order: Order): ValidationResult { /* ... */ }}Methods: Use verb phrases that describe the action (validate, calculate, fetch, send). Classes: Use nouns that describe the abstraction (Order, Customer, PaymentGateway). Variables: Use nouns describing content (pendingOrders, currentUser). Booleans: Use 'is/has/can/should' prefixes (isValid, hasPermission, canRetry).
Replace Conditional with Polymorphism
Replace Conditional with Polymorphism transforms complex switch statements or type-checking conditionals into polymorphic method calls. This is one of the most powerful object-oriented refactorings.
Motivation:
- Switch statements that check the same object type in multiple places
- If-else chains testing instance types (instanceof/typeof)
- Adding new types requires modifying existing code (violating Open-Closed Principle)
- Type-specific behavior is scattered across the codebase
The principle: When behavior varies by type, let the type itself define the behavior through polymorphism.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
// BEFORE: Switch statement on type codeenum EmployeeType { ENGINEER = 'ENGINEER', MANAGER = 'MANAGER', SALESPERSON = 'SALESPERSON'} class Employee { type: EmployeeType; baseSalary: number; commissionRate?: number; teamSize?: number; calculatePay(): number { switch (this.type) { case EmployeeType.ENGINEER: return this.baseSalary; case EmployeeType.MANAGER: return this.baseSalary + (this.teamSize! * 100); case EmployeeType.SALESPERSON: return this.baseSalary * (1 + this.commissionRate!); default: throw new Error(`Unknown employee type: ${this.type}`); } } getTitle(): string { switch (this.type) { case EmployeeType.ENGINEER: return 'Software Engineer'; case EmployeeType.MANAGER: return 'Engineering Manager'; case EmployeeType.SALESPERSON: return 'Sales Representative'; default: throw new Error(`Unknown employee type: ${this.type}`); } } // Every new employee type requires changes to EVERY switch statement} // AFTER: Polymorphic classesabstract class Employee { constructor(protected baseSalary: number) {} abstract calculatePay(): number; abstract getTitle(): string;} class Engineer extends Employee { calculatePay(): number { return this.baseSalary; } getTitle(): string { return 'Software Engineer'; }} class Manager extends Employee { constructor(baseSalary: number, private teamSize: number) { super(baseSalary); } calculatePay(): number { return this.baseSalary + this.teamSize * Manager.TEAM_BONUS_PER_MEMBER; } getTitle(): string { return 'Engineering Manager'; } private static readonly TEAM_BONUS_PER_MEMBER = 100;} class Salesperson extends Employee { constructor(baseSalary: number, private commissionRate: number) { super(baseSalary); } calculatePay(): number { return this.baseSalary * (1 + this.commissionRate); } getTitle(): string { return 'Sales Representative'; }} // Adding a new employee type: just add a new classclass Intern extends Employee { calculatePay(): number { return this.baseSalary * 0.5; // Interns get half } getTitle(): string { return 'Engineering Intern'; }} // Factory creates the right typeclass EmployeeFactory { static create(type: EmployeeType, ...args: any[]): Employee { switch (type) { case EmployeeType.ENGINEER: return new Engineer(args[0]); case EmployeeType.MANAGER: return new Manager(args[0], args[1]); case EmployeeType.SALESPERSON: return new Salesperson(args[0], args[1]); } }}Not all switch statements should become polymorphism. Keep conditionals when: (1) the behavior doesn't justify a class hierarchy, (2) the types are external and can't be extended, (3) there's only one conditional location, or (4) the domain is truly procedural. Apply polymorphism when the same conditional appears in multiple places.
Introduce Parameter Object
Introduce Parameter Object replaces a long parameter list with a single object that encapsulates those parameters. This pattern reduces parameter coupling and often reveals new abstractions.
Motivation:
- Methods with 4+ parameters are hard to understand and call correctly
- The same group of parameters appears in multiple method signatures
- Related parameters change together
- Parameter order is error-prone (positional arguments)
The insight: If the same parameters always travel together, they probably represent a concept that deserves its own name.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
// BEFORE: Long parameter lists repeated across methodsclass ReportGenerator { generateSalesReport( startDate: Date, endDate: Date, region: string, includeReturns: boolean, groupByCategory: boolean, format: ReportFormat ): Report { // ... } generateInventoryReport( startDate: Date, endDate: Date, region: string, includeInTransit: boolean, groupByWarehouse: boolean, format: ReportFormat ): Report { // ... } // What does a call look like? // generator.generateSalesReport(start, end, 'NA', true, false, 'PDF') // Which boolean is which? Easy to get wrong.} // AFTER: Parameter object encapsulates related datainterface DateRange { startDate: Date; endDate: Date;} interface SalesReportOptions { dateRange: DateRange; region: string; includeReturns: boolean; groupByCategory: boolean; format: ReportFormat;} interface InventoryReportOptions { dateRange: DateRange; region: string; includeInTransit: boolean; groupByWarehouse: boolean; format: ReportFormat;} class ReportGenerator { generateSalesReport(options: SalesReportOptions): Report { // Clearer access: options.dateRange.startDate // Easy to add new options without changing signature } generateInventoryReport(options: InventoryReportOptions): Report { // ... }} // Call sites are now self-documentingconst report = generator.generateSalesReport({ dateRange: { startDate: lastMonth, endDate: today }, region: 'NA', includeReturns: true, groupByCategory: false, format: 'PDF'}); // DateRange can grow behavior of its ownclass DateRange { constructor( public readonly startDate: Date, public readonly endDate: Date ) { this.validate(); } private validate(): void { if (this.startDate > this.endDate) { throw new Error('Start date must be before end date'); } } getDurationInDays(): number { const diffTime = this.endDate.getTime() - this.startDate.getTime(); return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); } contains(date: Date): boolean { return date >= this.startDate && date <= this.endDate; } overlaps(other: DateRange): boolean { return this.startDate <= other.endDate && other.startDate <= this.endDate; }}Often, introducing a parameter object reveals a missing domain concept. 'DateRange' is not just a convenience—it's a genuine business concept that deserves its own abstraction. The refactoring surfaces implicit concepts and makes them explicit.
Extract Interface
Extract Interface creates a new interface from the existing public methods of a class. This enables polymorphism without inheritance and supports dependency inversion.
Motivation:
- Need to support multiple implementations of the same concept
- Want to mock a class for testing
- Need to break a dependency cycle
- Class exposes too many methods; want to narrow the contract
- Preparing coding for the Dependency Inversion Principle
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
// BEFORE: Concrete class makes testing difficultclass PaymentGateway { async chargeCard( cardNumber: string, amount: number, currency: string ): Promise<PaymentResult> { // Real HTTP calls to payment provider const response = await fetch('https://payment.provider.com/charge', { method: 'POST', body: JSON.stringify({ cardNumber, amount, currency }), headers: { 'Authorization': `Bearer ${this.apiKey}` } }); return response.json(); } async refund(transactionId: string, amount: number): Promise<RefundResult> { // Real HTTP calls to payment provider }} class OrderService { constructor(private gateway: PaymentGateway) {} // Hard dependency async completeOrder(order: Order): Promise<void> { // This is hard to test - always makes real API calls const result = await this.gateway.chargeCard( order.paymentCardNumber, order.total, 'USD' ); // ... }} // AFTER: Interface allows substitutioninterface PaymentProcessor { chargeCard( cardNumber: string, amount: number, currency: string ): Promise<PaymentResult>; refund(transactionId: string, amount: number): Promise<RefundResult>;} // Real implementationclass StripePaymentGateway implements PaymentProcessor { constructor(private apiKey: string) {} async chargeCard( cardNumber: string, amount: number, currency: string ): Promise<PaymentResult> { // Real Stripe API calls } async refund(transactionId: string, amount: number): Promise<RefundResult> { // Real Stripe API calls }} // Test implementationclass FakePaymentGateway implements PaymentProcessor { private charges: { cardNumber: string; amount: number }[] = []; async chargeCard( cardNumber: string, amount: number, currency: string ): Promise<PaymentResult> { this.charges.push({ cardNumber, amount }); return { success: true, transactionId: 'fake-txn-123' }; } async refund(transactionId: string, amount: number): Promise<RefundResult> { return { success: true }; } // Test helper methods getCharges() { return this.charges; } simulateFailure() { /* ... */ }} class OrderService { constructor(private paymentProcessor: PaymentProcessor) {} // Depends on interface async completeOrder(order: Order): Promise<void> { const result = await this.paymentProcessor.chargeCard( order.paymentCardNumber, order.total, 'USD' ); // Now testable without real API calls }} // In tests:const fakeGateway = new FakePaymentGateway();const orderService = new OrderService(fakeGateway); await orderService.completeOrder(testOrder); expect(fakeGateway.getCharges()).toHaveLength(1);expect(fakeGateway.getCharges()[0].amount).toBe(testOrder.total);When extracting interfaces, include only the methods that clients actually need. If different clients use different subsets of a class's methods, create separate, focused interfaces. This aligns with the Interface Segregation Principle.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.