正在加载,请稍候…

Refactoring Techniques: Improving Code Without Changing Behavior

Learn systematic refactoring techniques to improve code structure. Extract methods, introduce parameters objects, replace conditionals with polymorphism, and more.

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.