Skip to content
Dr M H B Ariyaratne edited this page Jun 8, 2026 · 1 revision

SE-08: Refactoring

Part of the Software Engineering Principles series


What Is Refactoring?

Refactoring is the process of restructuring existing code without changing its observable behaviour. The goal is to improve internal quality — readability, maintainability, testability — while leaving the system's external behaviour identical.

Martin Fowler's definition: "A change made to the internal structure of software to make it easier to understand and cheaper to modify without changing its observable behaviour."

The two key words are "without changing observable behaviour." If you are fixing a bug, you are not refactoring — you are changing behaviour. If you are adding a feature, you are not refactoring — you are adding behaviour. Refactoring is a distinct activity, often done before or after making a functional change.


Why Refactor?

Technical Debt

Every shortcut taken during development accumulates as technical debt — code that works but is harder than it should be to understand or modify. Like financial debt, technical debt accrues interest: future changes take longer because the codebase is harder to work with.

Refactoring pays down technical debt.

Enabling Future Change

Code that is hard to read is hard to change safely. Refactoring restructures code to make the next feature or bug fix cheaper to implement.

The Relationship with Testing

Refactoring without tests is dangerous — you have no way to confirm that behaviour has not changed. The process is: write tests → refactor → run tests → confirm behaviour unchanged.


When to Refactor

The Rule of Three

  • First time: just do it
  • Second time you do something similar: note the duplication
  • Third time: refactor

Before Adding a Feature

Make the change easy, then make the easy change. If the code structure makes it hard to add a feature safely, refactor first to create the right structure, then add the feature.

When Fixing a Bug

When the code is confusing enough that you had to work hard to find the bug, refactor to make it obvious — then the bug becomes visible and can be fixed simply.

During Code Review

If a reviewer struggles to understand the code, that is a signal to refactor before merging.


Core Refactoring Techniques

Extract Method

The most common refactoring. Take a fragment of code and turn it into a method with a meaningful name.

// Before — one long method doing many things
public void processPharmacyOrder(Order order) {
    // Validate
    if (order.getItems().isEmpty()) throw new IllegalArgumentException("Empty order");
    for (OrderItem item : order.getItems()) {
        if (item.getQuantity() <= 0) throw new IllegalArgumentException("Invalid qty");
    }
    // Check stock
    for (OrderItem item : order.getItems()) {
        StockItem stock = stockRepo.find(item.getMedicine(), order.getDepartment());
        if (stock.getQuantity() < item.getQuantity()) {
            throw new InsufficientStockException(item.getMedicine());
        }
    }
    // Deduct stock and create bill
    for (OrderItem item : order.getItems()) {
        stockRepo.deduct(item);
    }
    Bill bill = billService.create(order);
    auditService.log(order, bill);
}

// After — each step is a named method
public void processPharmacyOrder(Order order) {
    validateOrder(order);
    checkStockAvailability(order);
    deductStock(order);
    Bill bill = billService.create(order);
    auditService.log(order, bill);
}

private void validateOrder(Order order) { ... }
private void checkStockAvailability(Order order) { ... }
private void deductStock(Order order) { ... }

The refactored version reads like a description of the process. A reader can understand what happens at a glance and drill into any step individually.


Rename

Rename a variable, method, or class to better describe what it is or does.

// Before
int d;
boolean flag;
void proc(Patient p) { }

// After
int appointmentExpiryDays;
boolean isInsuranceVerified;
void validatePatientEligibility(Patient patient) { }

Renaming is safe in modern IDEs — use the refactor-rename function so all references are updated atomically.


Extract Variable

Replace a complex expression with a named variable to explain its intent.

// Before — what does this condition mean?
if (bill.getCreatedDate().plusDays(30).isBefore(LocalDate.now())
        && bill.getStatus() == BillStatus.DRAFT
        && bill.getNetValue() > 0) { }

// After — the intent is clear
boolean isOverdueUnpaidBill = bill.getCreatedDate().plusDays(30).isBefore(LocalDate.now())
        && bill.getStatus() == BillStatus.DRAFT
        && bill.getNetValue() > 0;

if (isOverdueUnpaidBill) { }

Inline Method / Variable

The opposite of extract: replace a method or variable with its content when abstraction obscures more than it clarifies.

// An intermediary that adds no value
private boolean isGreaterThanZero(double value) {
    return value > 0;
}

// Just write the condition directly
if (amount > 0) { }

Move Method

Move a method to the class whose data it primarily uses.

// Bad — PatientBillingService knows a lot about Bill's internals
public class PatientBillingService {
    public boolean isOverdue(Bill bill) {
        return bill.getDueDate() != null
            && bill.getDueDate().isBefore(LocalDate.now())
            && bill.getStatus() != BillStatus.PAID;
    }
}

// Better — Bill knows about itself
public class Bill {
    public boolean isOverdue() {
        return dueDate != null
            && dueDate.isBefore(LocalDate.now())
            && status != BillStatus.PAID;
    }
}

Replace Conditional with Polymorphism

Replace type-checking switch/if chains with polymorphic method calls.

// Before — every new type requires modifying this method
public double calculateFee(Service service) {
    switch (service.getType()) {
        case CONSULTATION: return service.getBasePrice();
        case LAB_TEST: return service.getBasePrice() * 0.9;
        case RADIOLOGY: return service.getBasePrice() * 0.85;
        default: return service.getBasePrice();
    }
}

// After — each type knows its own fee calculation
public interface Service {
    double calculateFee();
}

public class Consultation implements Service {
    public double calculateFee() { return basePrice; }
}

public class LabTest implements Service {
    public double calculateFee() { return basePrice * 0.9; }
}

Introduce Parameter Object

Replace a long list of related parameters with a single object.

// Before
public List<Bill> findBills(Department dept, LocalDate from, LocalDate to,
                             BillStatus status, Patient patient) { }

// After
public List<Bill> findBills(BillSearchCriteria criteria) { }

Refactoring Safely

Step 1: Ensure Test Coverage

Before refactoring, you must have tests that cover the behaviour you are preserving. If tests do not exist, write characterisation tests first — tests that record what the code actually does so you can detect unintended changes.

Step 2: Make Small Steps

Each refactoring step should be small enough to verify immediately. Do not combine multiple refactorings into a single change.

Step 3: Run Tests After Each Step

After each small change, run the test suite. If a test fails, you know which step introduced the regression.

Step 4: Commit Refactoring Separately from Functional Changes

Mixing refactoring with feature additions makes code review harder and regression investigation harder. Commit the refactoring first (with a refactor: commit message), then make the functional change.


What Refactoring Is Not

Not refactoring Why
Rewriting from scratch Observable behaviour intentionally changes
Fixing a bug Behaviour changes by intent
Adding a feature Behaviour changes by intent
Performance optimisation Internal changes may change observable timing
Changing the database schema External contract change

Performance optimisation and schema changes may be valuable, but they are not refactoring and must be treated with appropriate care.


Previous: SE-07: Software Architecture
Next: SE-09: Testing Principles

Back to Software Engineering Principles

Clone this wiki locally