Refactoring Assistant Skill
This skill guides systematic code improvement through proven refactoring techniques. Use this whenever you need to clean up code, reduce technical debt, improve structure, or apply better design patterns.
Core Refactoring Principles
1. SOLID Principles
| Principle | Description | Violation Signs |
|---|---|---|
| Single Responsibility | One class = one reason to change | Large classes, mixed concerns |
| Open/Closed | Open for extension, closed for modification | Frequent edits to existing code |
| Liskov Substitution | Subtypes must be substitutable | Broken inheritance, type checks |
| Interface Segregation | Many specific interfaces > one general | Fat interfaces, unused methods |
| Dependency Inversion | Depend on abstractions, not concretions | Hard-coded dependencies |
2. DRY (Don't Repeat Yourself)
- Eliminate duplicate code
- Extract common logic into reusable components
- Use configuration over hardcoding
- Single source of truth for data
3. KISS (Keep It Simple, Stupid)
- Prefer simple solutions over clever ones
- Avoid premature optimization
- Reduce unnecessary complexity
- Write readable, self-documenting code
4. YAGNI (You Ain't Gonna Need It)
- Don't add functionality until needed
- Remove unused code
- Avoid speculative generality
- Build for today's requirements
Code Smells Catalog
Bloaters
Code that has grown excessively large:
| Smell | Description | Refactoring |
|---|---|---|
| Long Method | Method > 20-30 lines | Extract Method |
| Large Class | Class doing too much | Extract Class |
| Primitive Obsession | Overuse of primitives | Replace with Value Object |
| Long Parameter List | > 3-4 parameters | Parameter Object, Builder |
| Data Clumps | Groups of data appearing together | Extract Class |
Object-Orientation Abusers
Misuse of OOP principles:
| Smell | Description | Refactoring |
|---|---|---|
| Switch Statements | Complex conditionals | Replace with Polymorphism |
| Temporary Field | Fields only sometimes used | Extract Class |
| Refused Bequest | Subclass not using parent methods | Replace Inheritance with Delegation |
| Alternative Classes | Similar classes, different interfaces | Rename, Merge Classes |
Change Preventers
Code that makes changes difficult:
| Smell | Description | Refactoring |
|---|---|---|
| Divergent Change | One class changed for many reasons | Extract Class |
| Shotgun Surgery | One change affects many classes | Move Method, Inline Class |
| Parallel Inheritance | Subclass requires parallel subclass | Move Method, Move Field |
Dispensables
Code that could be removed:
| Smell | Description | Refactoring |
|---|---|---|
| Comments | Excessive comments hiding bad code | Extract Method, Rename |
| Duplicate Code | Same code in multiple places | Extract Method/Class |
| Dead Code | Unreachable/unused code | Delete it |
| Speculative Generality | Unused abstractions | Collapse Hierarchy |
| Lazy Class | Class doing too little | Inline Class |
Couplers
Excessive coupling between classes:
| Smell | Description | Refactoring |
|---|---|---|
| Feature Envy | Method uses another class more | Move Method |
| Inappropriate Intimacy | Classes too dependent | Move Method, Extract Class |
| Message Chains | a.b().c().d() | Hide Delegate |
| Middle Man | Class only delegates | Remove Middle Man |
Refactoring Patterns
Extract Method
Transform long methods into smaller, focused ones.
Before:
def process_order(order):
# Validate order
if not order.items:
raise ValueError("Order must have items")
if order.total < 0:
raise ValueError("Invalid total")
# Calculate discounts
discount = 0
if order.customer.is_premium:
discount = order.total * 0.1
if order.total > 100:
discount += order.total * 0.05
# Apply tax
subtotal = order.total - discount
tax = subtotal * 0.08
final_total = subtotal + tax
# Send confirmation
email = f"Order confirmed. Total: ${final_total}"
send_email(order.customer.email, "Order Confirmation", email)
return final_total
After:
def process_order(order):
validate_order(order)
discount = calculate_discount(order)
final_total = apply_tax(order.total - discount)
send_order_confirmation(order.customer, final_total)
return final_total
def validate_order(order):
if not order.items:
raise ValueError("Order must have items")
if order.total < 0:
raise ValueError("Invalid total")
def calculate_discount(order):
discount = 0
if order.customer.is_premium:
discount = order.total * 0.1
if order.total > 100:
discount += order.total * 0.05
return discount
def apply_tax(subtotal, tax_rate=0.08):
return subtotal * (1 + tax_rate)
def send_order_confirmation(customer, total):
email = f"Order confirmed. Total: ${total}"
send_email(customer.email, "Order Confirmation", email)
Extract Class
Split a class with multiple responsibilities.
Before:
class User:
def __init__(self, name, email, street, city, zip_code, country):
self.name = name
self.email = email
self.street = street
self.city = city
self.zip_code = zip_code
self.country = country
def get_full_address(self):
return f"{self.street}, {self.city}, {self.zip_code}, {self.country}"
def validate_address(self):
return bool(self.street and self.city and self.zip_code)
def format_mailing_label(self):
return f"{self.name}\n{self.get_full_address()}"
After:
class Address:
def __init__(self, street, city, zip_code, country):
self.street = street
self.city = city
self.zip_code = zip_code
self.country = country
def get_full_address(self):
return f"{self.street}, {self.city}, {self.zip_code}, {self.country}"
def is_valid(self):
return bool(self.street and self.city and self.zip_code)
class User:
def __init__(self, name, email, address: Address):
self.name = name
self.email = email
self.address = address
def format_mailing_label(self):
return f"{self.name}\n{self.address.get_full_address()}"
Replace Conditional with Polymorphism
Replace complex conditionals with inheritance.
Before:
class PaymentProcessor:
def process_payment(self, payment_type, amount, details):
if payment_type == "credit_card":
# Validate card
if not self._validate_card(details["card_number"]):
raise ValueError("Invalid card")
# Charge card
response = self._charge_card(details["card_number"], amount)
return response
elif payment_type == "paypal":
# Authenticate with PayPal
token = self._get_paypal_token(details["email"])
# Process PayPal payment
response = self._paypal_charge(token, amount)
return response
elif payment_type == "bank_transfer":
# Validate bank details
if not self._validate_iban(details["iban"]):
raise ValueError("Invalid IBAN")
# Initiate transfer
response = self._initiate_transfer(details["iban"], amount)
return response
else:
raise ValueError(f"Unknown payment type: {payment_type}")
After:
from abc import ABC, abstractmethod
class PaymentMethod(ABC):
@abstractmethod
def validate(self, details: dict) -> bool:
pass
@abstractmethod
def process(self, amount: float, details: dict) -> dict:
pass
class CreditCardPayment(PaymentMethod):
def validate(self, details):
return self._validate_card(details["card_number"])
def process(self, amount, details):
if not self.validate(details):
raise ValueError("Invalid card")
return self._charge_card(details["card_number"], amount)
class PayPalPayment(PaymentMethod):
def validate(self, details):
return bool(details.get("email"))
def process(self, amount, details):
token = self._get_paypal_token(details["email"])
return self._paypal_charge(token, amount)
class BankTransferPayment(PaymentMethod):
def validate(self, details):
return self._validate_iban(details["iban"])
def process(self, amount, details):
if not self.validate(details):
raise ValueError("Invalid IBAN")
return self._initiate_transfer(details["iban"], amount)
class PaymentProcessor:
def __init__(self):
self._methods = {
"credit_card": CreditCardPayment(),
"paypal": PayPalPayment(),
"bank_transfer": BankTransferPayment(),
}
def process_payment(self, payment_type, amount, details):
method = self._methods.get(payment_type)
if not method:
raise ValueError(f"Unknown payment type: {payment_type}")
return method.process(amount, details)
Introduce Parameter Object
Replace long parameter lists with an object.
Before:
def create_report(
title,
author,
start_date,
end_date,
include_charts,
include_summary,
page_size,
orientation,
department,
category
):
# Create report with all these parameters...
pass
def email_report(
title,
author,
start_date,
end_date,
include_charts,
include_summary,
recipients
):
# Email report with overlapping parameters...
pass
After:
from dataclasses import dataclass
from datetime import date
from typing import Optional
@dataclass
class ReportConfig:
title: str
author: str
start_date: date
end_date: date
include_charts: bool = True
include_summary: bool = True
@dataclass
class PrintConfig:
page_size: str = "A4"
orientation: str = "portrait"
@dataclass
class ReportFilter:
department: Optional[str] = None
category: Optional[str] = None
def create_report(
config: ReportConfig,
print_config: PrintConfig,
filter: ReportFilter
):
# Much cleaner!
pass
def email_report(
config: ReportConfig,
recipients: list[str]
):
# Reuses ReportConfig
pass
Replace Magic Numbers/Strings with Constants
Before:
def calculate_shipping(weight, distance):
if weight < 1:
base_cost = 5.99
elif weight < 5:
base_cost = 9.99
else:
base_cost = 14.99
if distance > 500:
base_cost *= 1.5
if distance > 1000:
base_cost *= 2.0
return base_cost
def get_status_message(status):
if status == 1:
return "Pending"
elif status == 2:
return "Processing"
elif status == 3:
return "Shipped"
elif status == 4:
return "Delivered"
After:
from enum import Enum, auto
from dataclasses import dataclass
# Weight-based pricing
class ShippingTier:
LIGHT_WEIGHT_LIMIT = 1 # kg
MEDIUM_WEIGHT_LIMIT = 5 # kg
LIGHT_COST = 5.99
MEDIUM_COST = 9.99
HEAVY_COST = 14.99
# Distance multipliers
class DistanceMultiplier:
LONG_DISTANCE_THRESHOLD = 500 # km
VERY_LONG_DISTANCE_THRESHOLD = 1000 # km
LONG_DISTANCE_MULTIPLIER = 1.5
VERY_LONG_DISTANCE_MULTIPLIER = 2.0
class OrderStatus(Enum):
PENDING = auto()
PROCESSING = auto()
SHIPPED = auto()
DELIVERED = auto()
@property
def display_name(self):
return self.name.capitalize()
def calculate_shipping(weight: float, distance: float) -> float:
if weight < ShippingTier.LIGHT_WEIGHT_LIMIT:
base_cost = ShippingTier.LIGHT_COST
elif weight < ShippingTier.MEDIUM_WEIGHT_LIMIT:
base_cost = ShippingTier.MEDIUM_COST
else:
base_cost = ShippingTier.HEAVY_COST
if distance > DistanceMultiplier.VERY_LONG_DISTANCE_THRESHOLD:
base_cost *= DistanceMultiplier.VERY_LONG_DISTANCE_MULTIPLIER
elif distance > DistanceMultiplier.LONG_DISTANCE_THRESHOLD:
base_cost *= DistanceMultiplier.LONG_DISTANCE_MULTIPLIER
return base_cost
def get_status_message(status: OrderStatus) -> str:
return status.display_name
Dependency Injection
Replace hard-coded dependencies with injected ones.
Before:
class UserService:
def __init__(self):
# Hard-coded dependencies - difficult to test
self.db = PostgresDatabase("localhost", "users_db")
self.cache = RedisCache("localhost:6379")
self.mailer = SmtpMailer("smtp.gmail.com")
def create_user(self, data):
user = self.db.insert("users", data)
self.cache.set(f"user:{user.id}", user)
self.mailer.send(user.email, "Welcome!")
return user
After:
from abc import ABC, abstractmethod
# Define interfaces (abstractions)
class Database(ABC):
@abstractmethod
def insert(self, table: str, data: dict) -> dict:
pass
class Cache(ABC):
@abstractmethod
def set(self, key: str, value: any) -> None:
pass
class Mailer(ABC):
@abstractmethod
def send(self, to: str, message: str) -> None:
pass
# Concrete implementations
class PostgresDatabase(Database):
def __init__(self, host: str, db_name: str):
self.host = host
self.db_name = db_name
def insert(self, table, data):
# PostgreSQL-specific implementation
pass
class RedisCache(Cache):
def __init__(self, url: str):
self.url = url
def set(self, key, value):
# Redis-specific implementation
pass
# Service with injected dependencies
class UserService:
def __init__(self, db: Database, cache: Cache, mailer: Mailer):
self.db = db
self.cache = cache
self.mailer = mailer
def create_user(self, data):
user = self.db.insert("users", data)
self.cache.set(f"user:{user.id}", user)
self.mailer.send(user.email, "Welcome!")
return user
# Easy to test with mocks!
class MockDatabase(Database):
def insert(self, table, data):
return {"id": 1, **data}
# Production usage
service = UserService(
db=PostgresDatabase("localhost", "users_db"),
cache=RedisCache("localhost:6379"),
mailer=SmtpMailer("smtp.gmail.com")
)
# Test usage
test_service = UserService(
db=MockDatabase(),
cache=MockCache(),
mailer=MockMailer()
)
Strategy Pattern
Encapsulate algorithms that can be swapped.
Before:
class PriceCalculator:
def calculate(self, items, customer_type, promo_code):
total = sum(item.price * item.quantity for item in items)
# Discount logic scattered and hard to maintain
if customer_type == "premium":
total *= 0.9 # 10% discount
elif customer_type == "vip":
total *= 0.8 # 20% discount
if promo_code == "SUMMER20":
total *= 0.8
elif promo_code == "FLASH50":
total *= 0.5
elif promo_code == "FREESHIP":
pass # Free shipping handled elsewhere
return total
After:
from abc import ABC, abstractmethod
class DiscountStrategy(ABC):
@abstractmethod
def apply(self, total: float) -> float:
pass
class NoDiscount(DiscountStrategy):
def apply(self, total):
return total
class PercentageDiscount(DiscountStrategy):
def __init__(self, percentage: float):
self.percentage = percentage
def apply(self, total):
return total * (1 - self.percentage / 100)
class FixedDiscount(DiscountStrategy):
def __init__(self, amount: float):
self.amount = amount
def apply(self, total):
return max(0, total - self.amount)
class CompositeDiscount(DiscountStrategy):
def __init__(self, *strategies: DiscountStrategy):
self.strategies = strategies
def apply(self, total):
for strategy in self.strategies:
total = strategy.apply(total)
return total
# Strategy factory
class DiscountFactory:
CUSTOMER_DISCOUNTS = {
"regular": NoDiscount(),
"premium": PercentageDiscount(10),
"vip": PercentageDiscount(20),
}
PROMO_DISCOUNTS = {
"SUMMER20": PercentageDiscount(20),
"FLASH50": PercentageDiscount(50),
"SAVE10": FixedDiscount(10),
}
@classmethod
def get_discount(cls, customer_type: str, promo_code: str = None) -> DiscountStrategy:
strategies = []
customer_discount = cls.CUSTOMER_DISCOUNTS.get(customer_type, NoDiscount())
strategies.append(customer_discount)
if promo_code:
promo_discount = cls.PROMO_DISCOUNTS.get(promo_code)
if promo_discount:
strategies.append(promo_discount)
if len(strategies) == 1:
return strategies[0]
return CompositeDiscount(*strategies)
class PriceCalculator:
def calculate(self, items, customer_type, promo_code=None):
total = sum(item.price * item.quantity for item in items)
discount = DiscountFactory.get_discount(customer_type, promo_code)
return discount.apply(total)
Null Object Pattern
Replace null checks with a null object.
Before:
class UserService:
def get_user(self, user_id):
user = self.db.find(user_id)
return user # Could be None
def display_welcome(self, user_id):
user = self.get_user(user_id)
if user is not None:
if user.name is not None:
print(f"Welcome, {user.name}!")
else:
print("Welcome!")
if user.preferences is not None:
if user.preferences.theme is not None:
apply_theme(user.preferences.theme)
else:
apply_theme("default")
else:
apply_theme("default")
else:
print("Welcome, Guest!")
apply_theme("default")
After:
from dataclasses import dataclass, field
@dataclass
class Preferences:
theme: str = "default"
language: str = "en"
notifications: bool = True
@dataclass
class User:
id: str
name: str
email: str
preferences: Preferences = field(default_factory=Preferences)
class NullUser(User):
"""Null Object representing a guest/anonymous user."""
def __init__(self):
super().__init__(
id="guest",
name="Guest",
email="",
preferences=Preferences()
)
def is_authenticated(self):
return False
class UserService:
NULL_USER = NullUser()
def get_user(self, user_id) -> User:
user = self.db.find(user_id)
return user if user else self.NULL_USER
def display_welcome(self, user_id):
user = self.get_user(user_id)
# No null checks needed!
print(f"Welcome, {user.name}!")
apply_theme(user.preferences.theme)
Refactoring Process
Step 1: Identify the Smell
- Review code for patterns listed above
- Check complexity metrics (cyclomatic complexity, LOC)
- Look for repeated patterns
Step 2: Ensure Test Coverage
- Write tests before refactoring (if missing)
- Tests should pass before AND after
- Use tests as a safety net
Step 3: Make Small Changes
- One refactoring at a time
- Commit after each successful change
- Keep changes reversible
Step 4: Verify Behavior
- Run all tests after each change
- Check for regressions
- Validate performance if relevant
Step 5: Clean Up
- Remove dead code
- Update documentation
- Review for additional improvements
Complexity Metrics
When to Refactor
| Metric | Threshold | Action |
|---|---|---|
| Cyclomatic Complexity | > 10 | Extract methods |
| Lines per Method | > 30 | Extract methods |
| Lines per Class | > 300 | Extract classes |
| Parameters | > 4 | Parameter object |
| Nesting Depth | > 3 | Early returns, extract |
| Duplicate Code | > 3 occurrences | Extract to shared |
Measuring Complexity
# Tools for measuring
# Python: radon, flake8-cognitive-complexity
# JavaScript: eslint-plugin-complexity
# General: SonarQube, CodeClimate
# Example: radon output
# M 42:0 process_order - C (13) # Complexity 13 = needs refactoring
# M 78:0 validate_input - A (3) # Complexity 3 = good
# Run with: radon cc src/ -a -s
Refactoring Checklist
Before refactoring:
- Tests exist and pass
- Code is version controlled
- Understand the current behavior
- Identified specific smell to fix
During refactoring:
- Make one change at a time
- Run tests after each change
- Commit frequently
- Keep behavior identical
After refactoring:
- All tests still pass
- Code is more readable
- Complexity is reduced
- Documentation updated
- No new bugs introduced
Output Format
When suggesting refactorings, provide:
## Refactoring Analysis
### Code Smell Identified
**Type:** [Smell name]
**Severity:** [High/Medium/Low]
**Location:** `file.py:42-78`
### Current Code Issues
1. [Issue 1]
2. [Issue 2]
3. [Issue 3]
### Recommended Refactoring
**Pattern:** [Refactoring pattern name]
**Current Code:**
```python
[Code before refactoring]
Refactored Code:
[Code after refactoring]
Benefits
- [Benefit 1]
- [Benefit 2]
- [Benefit 3]
Migration Steps
- [Step 1]
- [Step 2]
- [Step 3]
Testing Considerations
- [What tests to add/modify]
## Notes
- Always refactor with tests as a safety net
- Prefer many small refactorings over big rewrites
- Don't mix refactoring with adding new features
- Refactor incrementally, not all at once
- Sometimes "good enough" code is acceptable
- Consider team readability over clever solutions
- Document significant design decisions