Refactoring techniques, when to refactor, safe refactoring strategies, and code improvement patterns
Scope: Comprehensive guide to refactoring techniques, when to refactor, safe strategies, and systematic code improvement Lines: ~380 Last Updated: 2025-10-27 Format Version: 1.0 (Atomic)
Activate this skill when:
The Rule of Three
First time: Just write it Second time: Duplicate it (note the duplication) Third time: Refactor it (extract the pattern)
Refactor When:
Don't Refactor When:
The Refactoring Workflow:
1. Ensure tests exist and pass
↓
2. Make small, incremental change
↓
3. Run tests
↓
4. Commit if tests pass
↓
5. Repeat until complete
Critical Rules:
Extract Method/Function
Replace code fragment with a named function
Inline Function
Replace function call with function body (when function is trivial)
Extract Variable
Put complex expression result in self-explanatory variable
Inline Variable
Remove variable that's just as clear as expression
Rename
Change name to better reveal intent
Move Function
Move function to class that uses it most
Replace Conditional with Polymorphism
Replace type code with subclasses/interfaces
Before:
def print_owing(invoice):
print("***********************")
print("**** Customer Owes ****")
print("***********************")
outstanding = 0.0
for order in invoice.orders:
outstanding += order.amount
print(f"Name: {invoice.customer}")
print(f"Amount: {outstanding}")
After:
def print_owing(invoice):
print_banner()
outstanding = calculate_outstanding(invoice)
print_details(invoice, outstanding)
def print_banner():
print("***********************")
print("**** Customer Owes ****")
print("***********************")
def calculate_outstanding(invoice):
return sum(order.amount for order in invoice.orders)
def print_details(invoice, outstanding):
print(f"Name: {invoice.customer}")
print(f"Amount: {outstanding}")
Benefits: Each function has single responsibility, easier to test, reusable
Before:
function calculatePrice(quantity: number): number {
return quantity * 29.99 * 1.08; // What are these?
}
function checkStock(quantity: number): boolean {
return quantity < 100; // What does 100 mean?
}
After:
const PRICE_PER_ITEM = 29.99;
const TAX_RATE = 1.08;
const LOW_STOCK_THRESHOLD = 100;
function calculatePrice(quantity: number): number {
return quantity * PRICE_PER_ITEM * TAX_RATE;
}
function checkStock(quantity: number): boolean {
return quantity < LOW_STOCK_THRESHOLD;
}
Before:
func GetPayAmount(employee Employee) (float64, error) {
var result float64
if employee.IsSeparated {
result = 0
} else {
if employee.IsRetired {
result = employee.Pension
} else {
result = employee.Salary
}
}
return result, nil
}
After:
func GetPayAmount(employee Employee) (float64, error) {
if employee.IsSeparated {
return 0, nil
}
if employee.IsRetired {
return employee.Pension, nil
}
return employee.Salary, nil
}
Benefits: Reduces cognitive load, clarifies special cases, easier to read
Before:
class User:
def __init__(self, name, email, address, city, state, zip_code):
self.name = name
self.email = email
self.address = address
self.city = city
self.state = state
self.zip_code = zip_code
def get_full_address(self):
return f"{self.address}, {self.city}, {self.state} {self.zip_code}"
def validate_address(self):
# Complex address validation logic
pass
def format_address_for_label(self):
# Complex formatting logic
pass
After:
class Address:
def __init__(self, street, city, state, zip_code):
self.street = street
self.city = city
self.state = state
self.zip_code = zip_code
def get_full_address(self):
return f"{self.street}, {self.city}, {self.state} {self.zip_code}"
def validate(self):
# Validation logic
pass
def format_for_label(self):
# Formatting logic
pass
class User:
def __init__(self, name, email, address: Address):
self.name = name
self.email = email
self.address = address
Benefits: Single responsibility, address logic encapsulated, reusable
Before:
class Employee {
type: string; // "engineer", "manager", "salesperson"
getBonus(): number {
if (this.type === "engineer") {
return this.salary * 0.10;
} else if (this.type === "manager") {
return this.salary * 0.20;
} else if (this.type === "salesperson") {
return this.salary * 0.15 + this.commission;
}
return 0;
}
}
After:
abstract class Employee {
constructor(protected salary: number) {}
abstract getBonus(): number;
}
class Engineer extends Employee {
getBonus(): number {
return this.salary * 0.10;
}
}
class Manager extends Employee {
getBonus(): number {
return this.salary * 0.20;
}
}
class Salesperson extends Employee {
constructor(salary: number, private commission: number) {
super(salary);
}
getBonus(): number {
return this.salary * 0.15 + this.commission;
}
}
Benefits: Open/closed principle, easier to add new types, type-safe
Before:
def create_invoice(customer_name, customer_email, customer_address,
item_name, item_price, item_quantity,
tax_rate, discount_rate):
# 8 parameters is too many!
pass
After:
from dataclasses import dataclass
@dataclass
class Customer:
name: str
email: str
address: str
@dataclass
class LineItem:
name: str
price: float
quantity: int
@dataclass
class InvoiceOptions:
tax_rate: float
discount_rate: float
def create_invoice(customer: Customer, item: LineItem, options: InvoiceOptions):
# Much cleaner!
pass
Before:
func CalculateShipping(order Order, method string) float64 {
if method == "standard" {
return order.Weight * 0.50
} else if method == "express" {
return order.Weight * 1.50 + 10.00
} else if method == "overnight" {
return order.Weight * 3.00 + 25.00
}
return 0
}
After:
type ShippingStrategy interface {
Calculate(order Order) float64
}
type StandardShipping struct{}
func (s StandardShipping) Calculate(order Order) float64 {
return order.Weight * 0.50
}
type ExpressShipping struct{}
func (e ExpressShipping) Calculate(order Order) float64 {
return order.Weight * 1.50 + 10.00
}
type OvernightShipping struct{}
func (o OvernightShipping) Calculate(order Order) float64 {
return order.Weight * 3.00 + 25.00
}
func CalculateShipping(order Order, strategy ShippingStrategy) float64 {
return strategy.Calculate(order)
}
Before:
def process_payment(amount):
# Old implementation - keeping just in case
# def old_process_payment():
# charge_credit_card(amount)
# send_receipt()
# Current implementation
result = payment_gateway.process(amount)
if result.success:
send_receipt()
return result
# Unused function - was replaced 6 months ago
def legacy_process_payment(amount):
pass
After:
def process_payment(amount):
result = payment_gateway.process(amount)
if result.success:
send_receipt()
return result
# Old code deleted - it's in git history if needed!
Decompose Conditional:
# Before
if date.before(SUMMER_START) or date.after(SUMMER_END):
charge = quantity * winter_rate + winter_service_charge
else:
charge = quantity * summer_rate
# After
if is_winter(date):
charge = winter_charge(quantity)
else:
charge = summer_charge(quantity)
Consolidate Duplicate Conditional Fragments:
// Before
if (isSpecialDeal()) {
total = price * 0.95;
send();
} else {
total = price * 0.98;
send();
}
// After
if (isSpecialDeal()) {
total = price * 0.95;
} else {
total = price * 0.98;
}
send(); // Extracted from both branches
Replace Parameter with Method Call:
# Before
base_price = quantity * item_price
discount = get_discount(base_price, season)
# After
discount = get_discount(quantity, item_price, season)
def get_discount(quantity, item_price, season):
base_price = quantity * item_price
# Calculate discount
Preserve Whole Object:
// Before
low := daysTempRange.GetLow()
high := daysTempRange.GetHigh()
withinPlan := plan.WithinRange(low, high)
// After
withinPlan := plan.WithinRange(daysTempRange)
Replace Array with Object:
// Before
const row = ["Liverpool", 15];
const name = row[0];
const wins = row[1];
// After
interface Team {
name: string;
wins: number;
}
const team: Team = { name: "Liverpool", wins: 15 };
const name = team.name;
const wins = team.wins;
Encapsulate Field:
# Before
class Person:
name: str # Public field
person.name = "John"
# After
class Person:
def __init__(self):
self._name: str = ""
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, value: str):
if not value:
raise ValueError("Name cannot be empty")
self._name = value
person.name = "John"
Before Starting:
During Refactoring:
After Completing:
The Legacy Code Dilemma:
"We need tests to refactor safely, but we need to refactor to make code testable"
Solution: Characterization Tests
# Step 1: Write tests that capture current behavior (even if wrong)
def test_legacy_payment_calculation():
# Document current behavior, even if buggy
result = calculate_payment(100, "premium")
assert result == 95 # Current output (may be wrong!)
# Step 2: Now you can refactor safely
def calculate_payment(amount, tier):
# Refactor with confidence - tests will catch breaks
pass
# Step 3: Fix bugs in separate commits
def test_legacy_payment_calculation():
result = calculate_payment(100, "premium")
assert result == 90 # Fixed bug
Strangler Fig Pattern:
1. Identify module to replace
2. Create new implementation alongside old
3. Route new requests to new code
4. Gradually migrate existing data
5. Remove old code when fully migrated
❌ Big Bang Refactoring
→ Rewrite entire system at once (high risk!)
✅ Incremental refactoring (continuous improvement)
❌ Refactoring Without Tests
→ No safety net, likely to break things
✅ Write tests first, then refactor
❌ Mixing Refactoring and Features
→ Hard to review, hard to revert
✅ Separate commits: refactor, then feature
❌ Premature Optimization
→ Refactoring for imaginary performance issues
✅ Profile first, optimize hot paths only
❌ Over-Engineering
→ Introducing unnecessary abstractions
✅ Simplest thing that works
❌ Refactoring Under Deadline
→ Rushed refactoring introduces bugs
✅ Plan refactoring time explicitly
PyCharm / VS Code:
Automated Refactoring:
# Python: Rename symbol across codebase
rope --refactor rename old_name new_name
# Python: Extract method
rope --refactor extract_method
# Go: Rename
gopls rename -w old_name new_name
# TypeScript: Automated refactorings
npx ts-migrate rename old_name new_name
Mutation Testing (verify tests actually test):
# Python
pip install mutmut
mutmut run
# Check if tests catch intentional bugs
# If mutation score is low, tests are weak!
Snapshot Testing (ensure output unchanged):
def test_invoice_output_unchanged(snapshot):
invoice = create_test_invoice()
output = render_invoice(invoice)
snapshot.assert_match(output) # Fails if output changes