Handbooks / OOP & Design Patterns / Chapter 9
Code Smells and Refactoring to Patterns
7 pages · ~5 min✓ Reviewed
Builds on Behavioral Patterns. Next up: Anti-patterns and Thread-safe Singleton.
Code Smells: The Mental Model
1. Smell vs Bug
- A smell is a surface symptom of a deeper design problem
- The code works, but it is costly to change
- A smell is a prompt to investigate, not a bug
| Bug | Smell | |
|---|---|---|
| Behavior now | Broken | Works |
| Cost | Wrong result | Next change risky, slow, expensive |
2. Smell Families
| Family | Smells | Cost |
|---|---|---|
| Bloaters | Long Method, Large Class, Switch Statements, Primitive Obsession | Code grows too big |
| Change preventers | Shotgun Surgery, Divergent Change | One change, many edits |
| Couplers | Feature Envy | Classes lean on each other |
3. Refactoring Safely
- Refactoring = change internal structure, same observable behavior
Bloaters and Couplers: Spotting the Six Smells
1. Smells and Fixes
| Smell | Sign | Fix |
|---|---|---|
| Long Method | comments mark sections, 20+ lines | Extract Method |
| Large Class | too many fields, breaks SRP | Extract Class |
| Switch Statements | same type switch repeated, breaks Open/Closed | Polymorphism |
| Shotgun Surgery | one change, many classes | Move Method / Inline Class |
| Divergent Change |
Primitive Obsession and Cohesion
1. Primitive Obsession
- Primitive Obsession =
String,int,doublestanding in for money, email, phone, status code - Validation and behavior scatter across every caller
- Fix: Replace Primitive with Value Object: immutable, validates in its constructor
Money(amount, currency)ownsadd()andformat()
python
@dataclass(frozen=True) class Money: amount: Decimal currency: str def __post_init__(self): if self.amount < 0: raise ValueError def add(self, o): return Money(self.amount + o.amount, self.currency)
frozen = immutable; invalid state never exists
2. Type Codes
Refactoring to Strategy, State and Template Method
1. Conditional to Polymorphism
- Each switch branch becomes a subclass of one interface; callers lose the switch
- Strategy: client picks the algorithm, context delegates
- State: object changes behavior itself; replaces boolean flags
2. Strategy vs State
java
interface ShippingCost { double cost(Order o); }
class Express implements ShippingCost {...} // or Standard
double total() { return shipping.cost(this); }
class Pending implements OrderState {
public OrderState pay() { return new Paid(); }
}Strategy delegates; State returns the next state
Factory and Small Safe Refactoring Steps
1. Factory
- Replace Constructor with Factory Method
- Holds the single remaining switch: type code → Strategy or State
- Callers never use
newon concrete classes
python
def make_shipping(code): match code: case 'standard': return Standard() case 'express': return Express() case _: raise ValueError(code)
the only switch left
2. Safe Step Loop and Tests
- Tests green
- ONE small changemechanical
- Tests again
- Commitred = revert
Refactoring Cheat Sheet
1. Smell to Fix
| Smell | Fix |
|---|---|
| Long Method | Extract Method |
| Large Class | Extract Class |
| Switch | Replace Conditional with Polymorphism |
| Shotgun Surgery | Move Method / Inline Class |
| Feature Envy | Move Method |
| Primitive Obsession | Value Object / enum |
2. Pattern Pick
Check yourself
One commit turns a status switch into State classes and also adds a new Refunded status. A test fails. What went wrong?
- Refactor and feature are mixed, so the failure has two possible causes.
- Split them: refactor on green, commit, then add the feature.
The same shipping-type switch sits in 4 classes. You add Overnight. How many edits, and what removes the problem?
- 4 edits, one per copy: a Switch Statements smell.
- Replace Conditional with Polymorphism: one new Strategy class, plus one Factory switch.
An Order moves Pending, Paid, Shipped as methods are called on it. Strategy or State?
- State: the object switches itself and each state knows its successor.