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()
x → daysSinceLastLogin
Manager → EmailNotificationService
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.