Refactoring Skill
Core Principle
Refactoring changes code structure without changing behavior.
Refactoring is NOT about adding features or fixing bugs. It's about making code easier to understand, maintain, and extend. Every refactoring should be:
- •Safe — Behavior remains unchanged
- •Testable — Tests verify behavior preservation
- •Incremental — Small steps, frequent commits
- •Reversible — Can roll back if needed
When to Refactor
Good Times to Refactor
✅ Before adding a feature — Make room for new functionality ✅ During code review — Improve code before merge ✅ When you touch existing code — Leave it better than you found it ✅ When you see duplication — DRY principle ✅ When code is hard to test — Improve testability ✅ When you struggle to understand code — Simplify for future readers
Bad Times to Refactor
❌ While debugging — Fix the bug first, refactor later ❌ Near a deadline — Risk vs. reward is poor ❌ Without tests — Can't verify behavior preservation ❌ In unfamiliar code — Understand first, refactor later ❌ Just because — Must have a clear purpose
Refactoring Patterns
1. Extract Function
Problem: Long function doing multiple things
Solution: Extract logical blocks into named functions
Before:
def process_order(order):
# Validate
if not order.items:
raise ValueError("Order has no items")
if order.total < 0:
raise ValueError("Invalid total")
# Calculate
subtotal = sum(item.price * item.quantity for item in order.items)
tax = subtotal * 0.08
total = subtotal + tax
# Save
db.save(order)
send_confirmation_email(order.customer.email, order.id)
After:
def process_order(order):
validate_order(order)
calculate_totals(order)
save_and_notify(order)
def validate_order(order):
if not order.items:
raise ValueError("Order has no items")
if order.total < 0:
raise ValueError("Invalid total")
def calculate_totals(order):
subtotal = sum(item.price * item.quantity for item in order.items)
order.tax = subtotal * 0.08
order.total = subtotal + order.tax
def save_and_notify(order):
db.save(order)
send_confirmation_email(order.customer.email, order.id)
Benefits: Easier to test, understand, and modify each piece independently
2. Extract Variable
Problem: Complex expression that's hard to understand
Solution: Break into named intermediate variables
Before:
if (user.age >= 18 && user.country === 'US' &&
(user.verified || user.trustScore > 0.8)) {
allowPurchase();
}
After:
const isAdult = user.age >= 18;
const isUSUser = user.country === 'US';
const isTrusted = user.verified || user.trustScore > 0.8;
const canPurchase = isAdult && isUSUser && isTrusted;
if (canPurchase) {
allowPurchase();
}
Benefits: Self-documenting, easier to debug, reusable logic
3. Replace Magic Numbers
Problem: Unexplained numeric literals throughout code
Solution: Named constants with clear meaning
Before:
def calculate_shipping(weight):
if weight < 5:
return 4.99
elif weight < 20:
return 9.99
else:
return 14.99
After:
LIGHT_PACKAGE_THRESHOLD = 5 # pounds
MEDIUM_PACKAGE_THRESHOLD = 20 # pounds
LIGHT_SHIPPING_COST = 4.99
MEDIUM_SHIPPING_COST = 9.99
HEAVY_SHIPPING_COST = 14.99
def calculate_shipping(weight):
if weight < LIGHT_PACKAGE_THRESHOLD:
return LIGHT_SHIPPING_COST
elif weight < MEDIUM_PACKAGE_THRESHOLD:
return MEDIUM_SHIPPING_COST
else:
return HEAVY_SHIPPING_COST
Benefits: Clear meaning, easy to update, self-documenting
4. Simplify Conditional Logic
Problem: Nested or complex conditionals
Solution: Early returns, guard clauses, or strategy pattern
Before:
function calculateDiscount(user, order) {
if (user) {
if (user.isPremium) {
if (order.total > 100) {
return order.total * 0.2;
} else {
return order.total * 0.1;
}
} else {
if (order.total > 100) {
return order.total * 0.05;
}
}
}
return 0;
}
After:
function calculateDiscount(user, order) {
if (!user) return 0;
const discountRate = getDiscountRate(user, order);
return order.total * discountRate;
}
function getDiscountRate(user, order) {
if (user.isPremium) {
return order.total > 100 ? 0.2 : 0.1;
}
return order.total > 100 ? 0.05 : 0;
}
Benefits: Reduced nesting, clearer logic flow, easier to extend
5. Replace Duplication with Abstraction
Problem: Same code repeated in multiple places
Solution: Extract to shared function/class
Before:
def send_welcome_email(user):
subject = f"Welcome {user.name}!"
body = render_template("welcome.html", user=user)
smtp.send(user.email, subject, body)
log.info(f"Sent welcome email to {user.email}")
def send_reset_email(user, token):
subject = "Password Reset Request"
body = render_template("reset.html", user=user, token=token)
smtp.send(user.email, subject, body)
log.info(f"Sent reset email to {user.email}")
After:
def send_email(user, subject, template, **kwargs):
body = render_template(template, user=user, **kwargs)
smtp.send(user.email, subject, body)
log.info(f"Sent {template} email to {user.email}")
def send_welcome_email(user):
send_email(user, f"Welcome {user.name}!", "welcome.html")
def send_reset_email(user, token):
send_email(user, "Password Reset Request", "reset.html", token=token)
Benefits: DRY principle, single place to update email logic
6. Improve Naming
Problem: Unclear or misleading names
Solution: Descriptive, intention-revealing names
Before:
function calc(a, b) {
const tmp = a * b;
const res = tmp * 0.08;
return tmp + res;
}
After:
function calculateOrderTotal(subtotal, taxRate) {
const tax = subtotal * taxRate;
return subtotal + tax;
}
Benefits: Self-documenting, no need for comments
7. Split Large Classes
Problem: Class doing too many things (God Object)
Solution: Extract responsibilities into focused classes
Before:
class User:
def __init__(self, email, password):
self.email = email
self.password = password
def hash_password(self):
# Password hashing logic
pass
def send_email(self, subject, body):
# Email sending logic
pass
def save_to_db(self):
# Database logic
pass
def generate_report(self):
# Reporting logic
pass
After:
class User:
def __init__(self, email, password_hash):
self.email = email
self.password_hash = password_hash
class PasswordHasher:
@staticmethod
def hash(password):
# Password hashing logic
pass
class EmailService:
@staticmethod
def send(recipient, subject, body):
# Email sending logic
pass
class UserRepository:
@staticmethod
def save(user):
# Database logic
pass
class UserReportGenerator:
def generate(self, user):
# Reporting logic
pass
Benefits: Single Responsibility Principle, easier to test and maintain
Refactoring Workflow
Step 1: Ensure Tests Exist
Before refactoring:
# Run existing tests npm test # JavaScript pytest # Python cargo test # Rust # If tests don't exist or are insufficient: # STOP and write tests first
Rule: Never refactor without tests. Tests are your safety net.
Step 2: Make One Small Change
Keep refactorings atomic:
✅ Good: Extract one function ✅ Good: Rename one variable ✅ Good: Replace one magic number
❌ Bad: Extract function + rename variables + restructure class ❌ Bad: Refactor multiple files at once
Step 3: Run Tests After Each Change
After every refactoring step:
# Run tests immediately npm test # Tests should still pass # If tests fail: # 1. You changed behavior (not just structure) # 2. Revert and try again git checkout -- .
Step 4: Commit Frequently
Commit after each successful refactoring:
git add . git commit -m "refactor: extract validate_order function Extracted order validation logic into separate function for better testability and readability. No behavior change."
Commit message pattern:
refactor: <what you changed> <why you changed it>. No behavior change.
Step 5: Review Before Pushing
Before pushing refactored code:
- • All tests pass
- • No behavior changed (tests prove this)
- • Code is simpler/clearer
- • No dead code left behind
- • Documentation updated if needed
Code Smells (When to Refactor)
Smell: Long Function
Signs:
- •Function > 50 lines
- •Multiple levels of indentation
- •Hard to name because it does too many things
Refactoring: Extract Function, Extract Class
Smell: Duplicated Code
Signs:
- •Same code block in multiple places
- •Similar logic with minor variations
- •Copy-paste programming
Refactoring: Extract Function, Extract Superclass, Use Template Method
Smell: Large Class
Signs:
- •Class > 500 lines
- •Too many methods
- •Multiple reasons to change (violates SRP)
Refactoring: Extract Class, Split Responsibilities
Smell: Long Parameter List
Signs:
- •Function takes > 3 parameters
- •Parameters are related (should be object)
- •Adding parameters frequently
Refactoring: Introduce Parameter Object, Replace with Builder
Smell: Primitive Obsession
Signs:
- •Using primitives instead of small objects
- •Type codes (using int constants for types)
- •String parsing throughout codebase
Refactoring: Replace with Value Object, Extract Class
Smell: Feature Envy
Signs:
- •Method uses data from another class more than its own
- •Method would be better in another class
Refactoring: Move Method, Extract Method and Move
Safety Checklist
Before starting refactoring:
- • Tests exist and pass
- • Tests have good coverage (>80% for code being refactored)
- • You understand the code behavior
- • No deadline pressure
- • Git working directory is clean
During refactoring:
- • Making one small change at a time
- • Running tests after each change
- • Committing after each successful refactoring
- • No behavior changes (tests prove it)
After refactoring:
- • All tests pass
- • Code is simpler
- • No dead code remains
- • Documentation updated
- • Ready for code review
Integration with Other Skills
With Code Review
- •Refactor before requesting review
- •Self-review using code-review skill patterns
- •Smaller refactorings = easier reviews
With Testing Strategy
- •Write tests before refactoring (if missing)
- •Use test-driven refactoring
- •Tests prove behavior preservation
With Git Hygiene
- •Commit refactorings separately from features
- •Use "refactor:" prefix in commit messages
- •Keep refactoring commits small and focused
Common Pitfalls
❌ Refactoring without tests — Can't prove behavior unchanged ❌ Big bang refactoring — Too risky, hard to review ❌ Refactoring while adding features — Mix of concerns ❌ Refactoring unfamiliar code — Understand first ❌ Refactoring for perfection — Good enough is good enough ❌ Breaking backward compatibility — If public API, be careful
✅ Small, tested, incremental refactorings ✅ Clear purpose and benefit ✅ Preserve behavior ✅ Frequent commits
Remember: Refactoring is about making code better without changing what it does. Keep changes small, test frequently, commit often. If tests fail after refactoring, you changed behavior—revert and try again.