| name | refactoring-code-smells |
| description | Detects code smells and recommends specific refactorings. Covers the 22 canonical code smells from Fowler's Refactoring with Python examples. |
Code Smells
A code smell is a surface indication that usually corresponds to a deeper problem in the system. Smells don't always indicate a bug, but they suggest that the design could be improved.
When to Use This Skill
Triggers:
- You need to identify what's wrong with a piece of code
- Code review reveals structural concerns but you can't name the problem
- You want a systematic checklist of what to look for in a codebase
- You're planning a refactoring session and need to prioritize
Don't Triggers:
- You already know the smell and need the specific refactoring technique (use the appropriate sub-skill instead)
- You're writing new code from scratch
- The issue is a bug, not a design problem
The Code Smells Table
1. Duplicated Code
Signal: The same code structure appears in more than one place.
def calculate_us_tax(income):
if income < 10000:
return income * 0.1
elif income < 50000:
return income * 0.2
else:
return income * 0.3
def calculate_uk_tax(income):
if income < 10000:
return income * 0.1
elif income < 50000:
return income * 0.2
else:
return income * 0.3
Refactoring: Extract Method, Extract Superclass, Pull Up Method, Form Template Method.
2. Long Method
Signal: A method has many lines, or you need comments to explain sections within it.
def process_order(order):
if not order.items:
raise ValueError("Empty order")
if not order.customer:
raise ValueError("No customer")
total = 0
for item in order.items:
total += item.price * item.quantity
if item.quantity > 10:
total *= 0.95
if order.customer.is_vip:
total *= 0.9
payment_gateway.charge(order.customer, total)
email_service.send(order.customer.email, f"Order charged: {total}")
Refactoring: Extract Method, Replace Temp with Query, Introduce Parameter Object, Preserve Whole Object.
3. Large Class
Signal: A class has too many fields or methods. It tries to do too many things.
class Employee:
def __init__(self, name, address, phone, salary, tax_id):
self.name = name
self.address = address
self.phone = phone
self.salary = salary
self.tax_id = tax_id
self.projects = []
def calculate_pay(self): ...
def calculate_tax(self): ...
def calculate_bonus(self): ...
def generate_report(self): ...
def save_to_database(self): ...
def send_email(self, message): ...
def add_project(self, project): ...
def remove_project(self, project): ...
Refactoring: Extract Class, Extract Subclass, Extract Interface.
4. Long Parameter List
Signal: A function takes many parameters.
def create_user(name, email, phone, address, city, state, zip_code, country, role, department):
...
Refactoring: Replace Parameter with Method Call, Preserve Whole Object, Introduce Parameter Object.
@dataclass
class UserParams:
name: str
email: str
phone: str
address: str
city: str
state: str
zip_code: str
country: str
role: str
department: str
def create_user(params: UserParams):
...
5. Divergent Change
Signal: One class is commonly changed for different reasons.
class Report:
def __init__(self, data):
self.data = data
def fetch_data(self, query): ...
def format_html(self): ...
def format_csv(self): ...
def save_to_file(self, path): ...
Refactoring: Extract Class.
class ReportData:
def fetch(self, query): ...
class ReportFormatter:
def format_html(self, data): ...
def format_csv(self, data): ...
class ReportStorage:
def save_to_file(self, data, path): ...
6. Shotgun Surgery
Signal: Every time you make a change, you have to modify many classes.
def calculate_total(self):
return self.subtotal + self.subtotal * TaxRates.get_rate("standard")
def apply_tax(self):
self.tax = self.amount * TaxRates.get_rate("standard")
def calculate_tax(self):
return self.total * TaxRates.get_rate("standard")
Refactoring: Move Method, Move Field, Inline Class.
class TaxCalculator:
def calculate(self, amount, tax_type="standard"):
rate = self._get_rate(tax_type)
return amount * rate
7. Feature Envy
Signal: A method uses more data from another class than its own.
class Customer:
def get_shipping_cost(self, order):
base_cost = order.total * 0.1
if order.is_international:
base_cost += 50
if order.weight > 10:
base_cost += 20
return base_cost
Refactoring: Move Method.
class Order:
def get_shipping_cost(self):
base_cost = self.total * 0.1
if self.is_international:
base_cost += 50
if self.weight > 10:
base_cost += 20
return base_cost
8. Data Clumps
Signal: The same group of data items (fields, parameters) appears together repeatedly.
class Renderer:
def draw_point(self, x, y, z): ...
def calculate_distance(self, x1, y1, z1, x2, y2, z2): ...
def translate(self, x, y, z, dx, dy, dz): ...
Refactoring: Extract Class, Introduce Parameter Object, Preserve Whole Object.
@dataclass
class Point3D:
x: float
y: float
z: float
class Renderer:
def draw_point(self, point: Point3D): ...
def calculate_distance(self, p1: Point3D, p2: Point3D): ...
def translate(self, point: Point3D, delta: Point3D): ...
9. Primitive Obsession
Signal: Using primitive types (strings, ints) to represent domain concepts.
class Order:
def __init__(self):
self.status = "pending"
self.currency = "USD"
self.discount = 0.1
Refactoring: Replace Data Value with Object, Replace Type Code with Subclasses, Replace Type Code with State/Strategy.
from enum import Enum
class OrderStatus(Enum):
PENDING = "pending"
CONFIRMED = "confirmed"
SHIPPED = "shipped"
@dataclass(frozen=True)
class Currency:
code: str
@dataclass(frozen=True)
class Percentage:
value: float
10. Switch Statements
Signal: A switch/match statement or chain of if-elif that grows every time a new type is added.
class Employee:
def __init__(self, emp_type):
self.emp_type = emp_type
def calculate_bonus(employee):
if employee.emp_type == "engineer":
return employee.salary * 0.1
elif employee.emp_type == "manager":
return employee.salary * 0.2
elif employee.emp_type == "sales":
return employee.salary * 0.15
else:
return 0
Refactoring: Replace Conditional with Polymorphism, Replace Type Code with Subclasses.
class Employee(ABC):
@abstractmethod
def calculate_bonus(self) -> float: ...
class Engineer(Employee):
def calculate_bonus(self) -> float:
return self.salary * 0.1
class Manager(Employee):
def calculate_bonus(self) -> float:
return self.salary * 0.2
class Sales(Employee):
def calculate_bonus(self) -> float:
return self.salary * 0.15
11. Parallel Inheritance Hierarchies
Signal: Every time you make a subclass of one class, you must also make a subclass of another.
class Vehicle: ...
class Car(Vehicle): ...
class Truck(Vehicle): ...
class VehicleController: ...
class CarController(VehicleController): ...
class TruckController(VehicleController): ...
Refactoring: Move Method, Move Field — collapse one hierarchy by having the other reference it.
12. Lazy Class
Signal: A class that doesn't do enough to justify its existence.
class CustomerName:
def __init__(self, name: str):
self.name = name
def get_name(self):
return self.name
Refactoring: Inline Class or Collapse Hierarchy. If a subclass is the lazy class, use Collapse Hierarchy.
class Customer:
def __init__(self, name: str):
self.name = name
13. Speculative Generality
Signal: Code exists "just in case" — abstract classes nobody extends, methods nobody calls, parameters nobody uses.
class DataProcessor(ABC):
@abstractmethod
def process(self, data): ...
@abstractmethod
def validate(self, data): ...
@abstractmethod
def transform(self, data): ...
def supports_batch(self) -> bool:
return False
Refactoring: Collapse Hierarchy, Inline Class, Remove Parameter, Rename Method.
14. Temporary Field
Signal: A field is only used in certain circumstances or certain methods.
class OrderProcessor:
def __init__(self):
self.target_customer = None
def calculate_discount(self, customer):
self.target_customer = customer
discount = self._compute_discount()
self.target_customer = None
return discount
def _compute_discount(self):
if self.target_customer and self.target_customer.is_vip:
return 0.2
return 0.0
Refactoring: Extract Class, Introduce Null Object.
class DiscountCalculator:
def calculate(self, customer):
if customer.is_vip:
return 0.2
return 0.0
15. Message Chains
Signal: A series of calls like a.get_b().get_c().get_d().
class Order:
def get_customer(self): ...
order.get_customer().get_address().get_city()
Refactoring: Hide Delegate, Extract Method.
class Order:
def get_customer(self): ...
def get_customer_city(self):
return self.get_customer().get_address().get_city()
16. Middle Man
Signal: A class exists mostly to delegate to another class.
class Person:
def __init__(self, employee_record):
self._employee_record = employee_record
def get_name(self):
return self._employee_record.get_name()
def get_phone(self):
return self._employee_record.get_phone()
def get_department(self):
return self._employee_record.get_department()
Refactoring: Remove Middle Man, Inline Class.
class Person:
def __init__(self, employee_record):
self.employee_record = employee_record
17. Inappropriate Intimacy
Signal: One class reaches into another class's internals (accessing private fields, knowing too much about implementation).
class Order:
def apply_discount(self):
if self.customer._loyalty_points > 1000:
self.total *= 0.9
self.customer._loyalty_points -= 500
Refactoring: Move Method, Move Field, Change Bidirectional Association to Unidirectional, Extract Class, Hide Delegate.
class Customer:
def apply_loyalty_discount(self, total):
if self._loyalty_points > 1000:
self._loyalty_points -= 500
return total * 0.9
return total
class Order:
def apply_discount(self):
self.total = self.customer.apply_loyalty_discount(self.total)
18. Alternative Classes with Different Interfaces
Signal: Two classes do the same thing but have different method names.
class CustomerValidator:
def validate(self, customer): ...
class ClientChecker:
def check(self, client): ...
Refactoring: Rename Method, Move Method, Extract Superclass.
class Validator(ABC):
@abstractmethod
def validate(self, entity): ...
class CustomerValidator(Validator):
def validate(self, customer): ...
class ClientValidator(Validator):
def validate(self, client): ...
19. Incomplete Library Class
Signal: A library class is almost what you need but lacks a method you need.
Refactoring: Introduce Foreign Method, Introduce Local Extension.
class ExtendedStringUtils(StringUtils):
@staticmethod
def left_trim_and_pad(s: str, width: int) -> str:
return s.lstrip().ljust(width)
20. Data Class
Signal: A class has fields and getters/setters but no real behavior.
class Point:
def __init__(self, x, y):
self._x = x
self._y = y
def get_x(self): return self._x
def get_y(self): return self._y
def set_x(self, x): self._x = x
def set_y(self, y): self._y = y
Refactoring: Move Method, Encapsulate Field, Remove Setting Method, Extract Method.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def distance_to(self, other: "Point") -> float:
return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5
def translate(self, dx, dy):
return Point(self.x + dx, self.y + dy)
21. Refused Bequest
Signal: A subclass doesn't use or overrides most of what it inherits from its parent.
class EnhancedList(list):
def push(self, item):
self.append(item)
def pop_item(self):
return self.pop()
Refactoring: Replace Inheritance with Delegation.
class Stack:
def __init__(self):
self._items = []
def push(self, item):
self._items.append(item)
def pop(self):
return self._items.pop()
def is_empty(self):
return len(self._items) == 0
22. Comments
Signal: A comment exists to explain confusing code. The comment is a deodor — it masks the smell.
def calculate(order):
if order.customer.tenure_days > 730 and order.total_orders > 10:
return order.total * 0.85
return order.total
Refactoring: Extract Method, Introduce Explaining Variable, Rename Method.
def calculate(order):
if self._qualifies_for_loyalty_discount(order.customer):
return order.total * (1 - LOYALTY_DISCOUNT_RATE)
return order.total
def _qualifies_for_loyalty_discount(self, customer):
return customer.tenure_years >= 2 and customer.total_orders > 10
Quick Reference — Smell → Refactoring Map
| Smell | Primary Refactorings |
|---|
| Duplicated Code | Extract Method, Extract Superclass, Pull Up Method |
| Long Method | Extract Method, Replace Temp with Query |
| Large Class | Extract Class, Extract Subclass, Extract Interface |
| Long Parameter List | Introduce Parameter Object, Preserve Whole Object |
| Divergent Change | Extract Class |
| Shotgun Surgery | Move Method, Move Field, Inline Class |
| Feature Envy | Move Method |
| Data Clumps | Extract Class, Introduce Parameter Object |
| Primitive Obsession | Replace Data Value with Object |
| Switch Statements | Replace Conditional with Polymorphism |
| Parallel Inheritance Hierarchies | Move Method, Move Field |
| Lazy Class | Inline Class, Collapse Hierarchy |
| Speculative Generality | Collapse Hierarchy, Inline Class, Remove Parameter |
| Temporary Field | Extract Class, Introduce Null Object |
| Message Chains | Hide Delegate, Extract Method |
| Middle Man | Remove Middle Man, Inline Class |
| Inappropriate Intimacy | Move Method, Extract Class, Hide Delegate |
| Alternative Classes | Rename Method, Extract Superclass |
| Incomplete Library Class | Introduce Foreign Method, Introduce Local Extension |
| Data Class | Move Method, Encapsulate Field |
| Refused Bequest | Replace Inheritance with Delegation |
| Comments | Extract Method, Rename Method, Introduce Explaining Variable |
Source: Martin Fowler, "Refactoring: Improving the Design of Existing Code" (2nd Edition)