"Refactoring is a disciplined technique for restructuring code without changing its external behavior." — Martin Fowler
Refactoring isn't about rewriting everything from scratch. It's about making small, safe improvements that compound over time.
When to Refactor
✅ Refactor When:
- Adding a new feature — Clean the area first ("Boy Scout Rule")
- Fixing a bug — If code is hard to understand, refactor before fixing
- Code review — When you notice duplication or complexity
- Performance issues — After profiling identifies bottlenecks
- Before a major release — Clean up technical debt
❌ Don't Refactor When:
- Just before a deadline — Risk vs. reward isn't worth it
- Code is stable and rarely changed — "If it ain't broke..."
- You don't have tests — Write tests first!
- It's easier to rewrite — Sometimes a fresh start is better
Common Code Smells
1. Long Method
// ❌ Smell: 150-line function
function processOrder(order) {
// Validate order
// ...50 lines
// Calculate totals
// ...40 lines
// Apply discounts
// ...30 lines
// Save to database
// ...30 lines
}
// ✅ Refactored: Extract methods
function processOrder(order) {
validateOrder(order);
const totals = calculateTotals(order);
const finalPrice = applyDiscounts(totals, order.coupon);
return saveOrder(order, finalPrice);
}
2. Duplicate Code
// ❌ Smell: Same logic repeated
function createUser(data) {
const user = { ...data };
user.createdAt = new Date();
user.updatedAt = new Date();
user.status = 'active';
return db.save(user);
}
function createAdmin(data) {
const admin = { ...data };
admin.createdAt = new Date();
admin.updatedAt = new Date();
admin.status = 'active';
admin.role = 'admin';
return db.save(admin);
}
// ✅ Refactored: Extract common logic
function createUserBase(data, role = 'user') {
return {
...data,
createdAt: new Date(),
updatedAt: new Date(),
status: 'active',
role,
};
}
function createUser(data) {
return db.save(createUserBase(data));
}
function createAdmin(data) {
return db.save(createUserBase(data, 'admin'));
}
3. Long Parameter List
// ❌ Smell: Too many parameters
function createInvoice(
customerId,
items,
taxRate,
discount,
currency,
paymentMethod,
shippingAddress,
billingAddress,
notes
) {
// ...
}
// ✅ Refactored: Use object parameter
function createInvoice(options) {
const {
customerId,
items,
taxRate = 0.21,
discount = 0,
currency = 'EUR',
paymentMethod,
shippingAddress,
billingAddress,
notes = '',
} = options;
// ...
}
4. Large Class
// ❌ Smell: Class does everything
class User {
login() { }
logout() { }
sendEmail() { }
saveToDatabase() { }
generatePDF() { }
processPayment() { }
uploadImage() { }
}
// ✅ Refactored: Split responsibilities
class User {
constructor(data) {
this.data = data;
}
}
class UserAuth {
login(user) { }
logout(user) { }
}
class UserRepository {
save(user) { }
find(id) { }
}
class EmailService {
send(to, subject, body) { }
}
class PaymentProcessor {
charge(user, amount) { }
}
5. Primitive Obsession
// ❌ Smell: Using primitives instead of objects
function calculateShipping(
weight, // number (kg)
distance, // number (km)
isExpress, // boolean
country // string
) {
// ...
}
// ✅ Refactored: Value objects
class Weight {
constructor(kg) {
if (kg <= 0) throw new Error('Invalid weight');
this.kg = kg;
}
}
class Distance {
constructor(km) {
if (km <= 0) throw new Error('Invalid distance');
this.km = km;
}
}
class ShippingOption {
constructor(isExpress, country) {
this.isExpress = isExpress;
this.country = country;
}
}
function calculateShipping(weight, distance, option) {
// Type safety + validation built-in
}
6. Feature Envy
// ❌ Smell: Method more interested in another class
class Order {
getTotalPrice() {
let total = 0;
for (const item of this.items) {
total += item.quantity * item.product.price;
}
return total;
}
}
// ✅ Refactored: Move logic to appropriate class
class OrderItem {
getPrice() {
return this.quantity * this.product.price;
}
}
class Order {
getTotalPrice() {
return this.items.reduce((sum, item) => sum + item.getPrice(), 0);
}
}
Refactoring Techniques
1. Extract Method
// Before
function printOwing() {
printBanner();
let outstanding = 0;
for (const order of orders) {
outstanding += order.amount;
}
console.log(`Name: ${name}`);
console.log(`Amount: ${outstanding}`);
}
// After
function printOwing() {
printBanner();
const outstanding = calculateOutstanding();
printDetails(outstanding);
}
function calculateOutstanding() {
return orders.reduce((sum, order) => sum + order.amount, 0);
}
function printDetails(outstanding) {
console.log(`Name: ${name}`);
console.log(`Amount: ${outstanding}`);
}
2. Replace Conditional with Polymorphism
// Before
function getSpeed(vehicle) {
switch (vehicle.type) {
case 'car':
return vehicle.enginePower * 1.2;
case 'boat':
return vehicle.enginePower * 0.8;
case 'plane':
return vehicle.enginePower * 2.0;
}
}
// After
class Vehicle {
getSpeed() {
throw new Error('Must implement getSpeed');
}
}
class Car extends Vehicle {
getSpeed() {
return this.enginePower * 1.2;
}
}
class Boat extends Vehicle {
getSpeed() {
return this.enginePower * 0.8;
}
}
class Plane extends Vehicle {
getSpeed() {
return this.enginePower * 2.0;
}
}
3. Introduce Parameter Object
// Before
function createDateRange(startYear, startMonth, startDay, endYear, endMonth, endDay) {
// ...
}
// After
class DateRange {
constructor(start, end) {
this.start = start;
this.end = end;
}
}
function createDateRange(range) {
// ...
}
Safe Refactoring Process
1. Write Tests First
// Before refactoring, ensure tests exist
test('calculates total with discount', () => {
const order = new Order([item1, item2]);
expect(order.getTotalWithDiscount(0.1)).toBe(90);
});
2. Make Small Changes
Refactor in tiny steps. After each step:
- Run tests
- Commit if tests pass
- Continue
3. Don't Refactor and Add Features Simultaneously
Separate commits:
- Commit 1: Refactor (tests stay green)
- Commit 2: Add new feature
Tools to Help Refactoring
IDE Refactoring Tools
- VS Code: Rename symbol, extract method, move file
- IntelliJ IDEA: Advanced refactorings (extract interface, inline, etc.)
- TypeScript: Refactoring with type safety
Linters and Static Analysis
- ESLint: Detect code smells
- SonarQube: Technical debt tracking
- CodeClimate: Automated code review
- ScanMyCode.dev: AI-powered refactoring recommendations
Measuring Refactoring Success
Metrics to Track
- Cyclomatic complexity: Lower is better (<10 per function)
- Code coverage: Should stay same or improve
- Build time: Shouldn't increase significantly
- Bug rate: Should decrease over time
- Team velocity: Should improve as code gets cleaner
Common Refactoring Mistakes
❌ Big Bang Refactoring
Rewriting entire modules at once. High risk, hard to review, difficult to merge.
❌ Refactoring Without Tests
How do you know you didn't break anything?
❌ Premature Optimization
Don't refactor for performance without profiling data.
❌ Ignoring the Team
Large refactorings need team buy-in and coordination.
The Boy Scout Rule
"Leave the code cleaner than you found it."
Don't wait for the perfect time to refactor. Make small improvements every time you touch code:
- Rename a variable for clarity
- Extract a long method
- Remove dead code
- Fix a typo in comments
Conclusion
Refactoring is continuous improvement, not a one-time event. Start small, test often, and make incremental progress. Your future self will thank you.
Need help identifying refactoring opportunities in your codebase? Get a code quality audit and receive specific refactoring recommendations within 24 hours.