Part 3: Refactoring Techniques and Patterns

Series Navigation: ← Part 2: Code Review Process | Part 4: Testing During Refactoring β†’

Introduction

In Parts 1 and 2, we established principles and review processes. Now it's time to get hands-on with actual refactoring techniques. These are patterns I've applied repeatedly across my TypeScript microservices, transforming legacy code into maintainable, testable systems.

A year ago, I inherited a payment processing service with a single 2,000-line file containing all the business logic. Through systematic refactoring using the techniques in this article, we transformed it into a well-organized service with proper separation of concerns. The result? Bug reports dropped by 70%, and new features that previously took weeks now take days.

The Refactoring Process

Before You Start

Rule #1: Never refactor without tests.

If tests don't exist, write them first. I learned this the hard way when I "improved" our order service and broke production. Now I follow this workflow religiously:

  1. Write/verify tests for existing behavior

  2. Refactor with tests passing

  3. Verify tests still pass after refactoring

  4. No new functionality during refactoring

The Safety Net

// Step 1: Add tests for current behavior (even if it's ugly)
describe('OrderService - Current Behavior', () => {
  it('should calculate total with tax and shipping', async () => {
    const service = new OrderService();
    const result = await service.calculateTotal({
      items: [
        { price: 100, quantity: 2 },
        { price: 50, quantity: 1 }
      ],
      state: 'CA',
      shippingMethod: 'standard'
    });
    
    // Document current behavior, even if it seems wrong
    expect(result).toBe(272.25); // 250 + 21.25 tax + 1 shipping
  });
});

Once tests are in place, refactor with confidence.

Technique 1: Extract Method

Problem: Long methods doing multiple things

This is the most common refactoring I perform. Functions grow organically until they're doing five different things.

Before: God Method

From my authentication service:

After: Extracted Methods

Benefits:

  • Main method reads like a story

  • Each method has single purpose

  • Easy to test individual steps

  • Easy to modify one step without affecting others

Technique 2: Extract Class

Problem: A class doing too many things

Before: God Class

From my e-commerce service:

This class has at least 5 different responsibilities!

After: Extracted Classes

Benefits:

  • Each class has single responsibility

  • Easy to test in isolation

  • Can reuse components (TaxCalculator in other services)

  • Changes to tax logic don't affect shipping logic

Technique 3: Replace Conditional with Polymorphism

Problem: Large if/else or switch statements

This pattern appears frequently in payment and notification systems.

Before: Conditional Hell

Adding a new payment method requires modifying this function!

After: Polymorphic Strategy

Benefits:

  • Adding new payment method = create new class

  • No modification of existing code (Open/Closed Principle)

  • Each provider is independently testable

  • Clear separation of concerns

Technique 4: Introduce Parameter Object

Problem: Functions with many parameters

Before: Parameter Explosion

After: Parameter Object

Benefits:

  • Self-documenting

  • Easy to add optional fields

  • Type-safe with TypeScript

  • Can validate the entire object

Technique 5: Replace Magic Numbers with Constants

Problem: Unexplained numbers in code

Before: Magic Everywhere

What do these numbers mean?

After: Named Constants

Benefits:

  • Code explains itself

  • Easy to modify values

  • Constants can be moved to configuration

  • Reduces copy-paste errors

Technique 6: Null Object Pattern

Problem: Null checks everywhere

Before: Null Checks Everywhere

After: Null Object Pattern

Real-World Refactoring: Complete Example

Here's a complete refactoring from my inventory service:

Before (380 lines in one file)

After (Split into focused modules)

Improvements:

  • Domain models capture business logic

  • Repositories handle persistence

  • Service orchestrates workflow

  • Each class has single responsibility

  • Easy to test each component

  • Type-safe throughout

  • Better error handling

Conclusion

Refactoring is a continuous practice, not a one-time event. Key techniques covered:

  1. Extract Method: Break long functions into focused ones

  2. Extract Class: Separate responsibilities into different classes

  3. Replace Conditional with Polymorphism: Use strategy pattern for variations

  4. Introduce Parameter Object: Group related parameters

  5. Replace Magic Numbers: Use named constants

  6. Null Object Pattern: Eliminate null checks

In Part 4, we'll cover testing strategies during refactoring to ensure we don't break existing functionality.

Series Navigation: ← Part 2: Code Review Process | Part 4: Testing During Refactoring β†’

Last updated