Refactoring & Code Quality
Code Smells
A smell is a surface symptom pointing at a deeper design problem — not a bug, and not always wrong, but a signal worth investigating. Naming them gives a team shared vocabulary for "something here is making change harder than it should be."
- A smell doesn't mean the code is incorrect — it means it resists change, which is where the real cost shows up
- Duplicated Code is the most fundamental smell — nearly every other smell either causes it or is caused by it
- Long Method / Large Class: size alone isn't the problem, low cohesion inside that size is
- Feature Envy: a method more interested in another class's data than its own — a sign it's living in the wrong place
- Divergent Change (one class changes for many unrelated reasons) and Shotgun Surgery (one reason to change touches many classes) are mirror-image cohesion problems
- Smells guide which refactoring to reach for — see Refactoring Catalog Core for the mechanics
| Smell | What it looks like | Likely refactoring |
|---|---|---|
| Duplicated Code | Same expression/structure copy-pasted | Extract Method, then call it from both places |
| Long Method | A method that needs scrolling to read | Extract Method on cohesive chunks |
| Long Parameter List | Four, five, six positional parameters | Introduce Parameter Object |
| Feature Envy | Method reaches into another object's data repeatedly | Move Method to the object it envies |
| Data Clumps | The same 3–4 fields always travel together as parameters | Extract a class for the clump |
| Primitive Obsession | A String/int standing in for a real concept (money, email) | Replace primitive with a small value type |
| Switch Statements | The same type-code switch repeated at several call sites | Replace Conditional with Polymorphism |
Smells are heuristics, not indictments — a 40-line method that reads top-to-bottom as one clear narrative is not automatically a "Long Method" problem; a Utils class gathering three unrelated one-line helpers might be a pragmatic trade-off, not a crisis. The judgment call smells demand is: does this shape make the next change harder than it needs to be? If the answer is no, leave it — refactoring for its own sake, on code nobody is touching, is pure cost with no payoff (see Technical Debt on when paying down debt is actually worth it).
// Envious: OrderPrinter cares more about Order's internals than its own.
class OrderPrinter {
String describe(Order order) {
return order.getCustomerName() + " ordered " + order.getItems().size()
+ " items totaling quot; + order.getItems().stream().mapToDouble(Item::getPrice).sum();
}
}
// Fixed: the logic moves to live with the data it uses (Move Method).
class Order {
String describe() {
return customerName + " ordered " + items.size()
+ " items totaling quot; + items.stream().mapToDouble(Item::getPrice).sum();
}
}