Refactoring Basics: Making Code Better Without Breaking It

You’re reading code you wrote three months ago and barely recognize it. Or worse, you’re reading someone else’s code and it’s doing something simple in the most complicated way possible. That’s when you need refactoring.

What This Is

Refactoring is improving the internal structure of code without changing what it does. You’re making it easier to understand, easier to modify, and easier to maintain. The tests pass before you start. They still pass when you’re done. But the code in between is cleaner.

It’s not adding features. It’s not fixing bugs. It’s making the code better at being code.

Martin Fowler, who wrote the book on this (literally), defines it as “a change made to the internal structure of software to make it easier to understand and cheaper to modify without changing its observable behavior.”

Minimum Viable Understanding

Here’s what you need to know right now:

When code smells, make it better. If you have to read a function three times to understand what it does, that’s a smell. If you copy-paste code because it’s easier than understanding the original, that’s a smell. If you’re afraid to change something because you don’t know what else might break, that’s definitely a smell.

Make small, safe changes. Don’t refactor and add features at the same time. Don’t refactor without tests. Don’t change everything at once. One small improvement, verify it works, commit. Repeat.

Tests are your safety net. If you don’t have tests, you’re not refactoring. You’re just moving code around and hoping. Write tests first if you need to, then refactor.

The Boy Scout Rule applies: leave code cleaner than you found it. You don’t have to fix everything. But if you touch a file, make it slightly better while you’re there.

Real Red Flags

Here’s what bad code looks like versus what good code looks like:

Long Methods

Before: A method that scrolls off the screen

def process_order(order_id, customer_id, items, payment_method,
                  shipping_address, billing_address, discount_code,
                  gift_wrap, gift_message, email_notifications):
    # 150 lines of code doing everything from
    # validation to payment to inventory to shipping
    # Good luck understanding what happens where
    ...

After: Small methods that each do one thing

def process_order(order_data):
    validate_order(order_data)
    payment = process_payment(order_data.payment_info)
    inventory = reserve_inventory(order_data.items)
    shipment = create_shipment(order_data.shipping_info)
    send_confirmation(order_data.customer, shipment)
    return Order(payment, inventory, shipment)

Duplicate Code

Before: Same logic in multiple places

// In user-controller.js
if (user.age < 18 || user.region === 'EU' && !user.gdprConsent) {
    return forbidden();
}

// In api-controller.js
if (user.age < 18 || user.region === 'EU' && !user.gdprConsent) {
    return forbidden();
}

// In batch-processor.js
if (user.age < 18 || user.region === 'EU' && !user.gdprConsent) {
    return forbidden();
}

After: Logic in one place

function canUserAccessFeature(user) {
    if (user.age < 18) return false;
    if (user.region === 'EU' && !user.gdprConsent) return false;
    return true;
}

// Now everywhere just calls:
if (!canUserAccessFeature(user)) {
    return forbidden();
}

Cryptic Names

Before: Names that tell you nothing

public void doIt(List<Thing> stuff, int x) {
    for (Thing t : stuff) {
        if (t.getVal() > x) {
            proc(t);
        }
    }
}

After: Names that explain themselves

public void processHighValueOrders(List<Order> orders, int minimumValue) {
    for (Order order : orders) {
        if (order.getTotalValue() > minimumValue) {
            fulfillOrder(order);
        }
    }
}

The Big Three Refactorings

If you learn three refactoring techniques, make it these:

1. Extract Method

Take a chunk of code doing something specific and pull it into its own method with a name that explains what it does.

Before:

def generate_report(users):
    report = []
    for user in users:
        # Calculate metrics
        total_spent = sum(order.amount for order in user.orders)
        average_order = total_spent / len(user.orders) if user.orders else 0
        days_active = (datetime.now() - user.created_at).days

        report.append({
            'name': user.name,
            'total': total_spent,
            'average': average_order,
            'days': days_active
        })
    return report

After:

def generate_report(users):
    return [calculate_user_metrics(user) for user in users]

def calculate_user_metrics(user):
    total_spent = sum(order.amount for order in user.orders)
    average_order = total_spent / len(user.orders) if user.orders else 0
    days_active = (datetime.now() - user.created_at).days

    return {
        'name': user.name,
        'total': total_spent,
        'average': average_order,
        'days': days_active
    }

Now calculate_user_metrics can be tested independently and reused elsewhere.

2. Remove Duplication

When you see the same code in multiple places, extract it to one place. Future bugs only need one fix, not three.

This is so important that it has multiple names: DRY (Don’t Repeat Yourself), Once and Only Once, Rule of Three (okay to duplicate once, but the third time you extract it).

3. Rename for Clarity

Variables, functions, and classes should say what they are and what they do. If you need a comment to explain it, the name is wrong.

getUserData()fetchUserProfileFromDatabase() xdaysSinceLastLogin ManagerEmailNotificationService

Future you will thank present you.

Quick Validation Test

Ask yourself these questions:

Can someone unfamiliar with this code understand what it does in under a minute? If not, names need work or methods need extraction.

If a requirement changes, do you know exactly where to make the change? If not, you probably have duplication or poor separation of concerns.

Can you write a test for this code without testing implementation details? If not, it’s probably doing too much or is too tightly coupled.

Would you be comfortable explaining this code to your manager? If not, it might be overly clever or solving problems that don’t exist.

When to Refactor

Do refactor when:

  • You’re about to add a feature and the code is hard to work with
  • You just finished a feature and left some mess behind
  • You’re doing code review and spot a clear improvement
  • You’re fixing a bug and realize the code made the bug easy to introduce

Don’t refactor when:

  • You have no tests and no time to write them
  • The code works fine and nobody touches it (if it ain’t broke and nobody’s looking at it, leave it)
  • You’re on a deadline and the refactoring isn’t blocking you
  • You just want to use a new pattern you learned (refactor to solve problems, not to show off)

One-Sentence Maxim

Leave the code cleaner than you found it.

You don’t have to fix everything. You can’t fix everything. But every small improvement compounds. Six months of small improvements transforms a codebase.

You've finished reading this surface level content