Reading Code Smells
Smells are triggers, not laws
Section titled “Smells are triggers, not laws”You now know how to refactor safely. The missing piece is knowing what to refactor and when. The answer is code smells: surface signs that something underneath may be poorly structured. The word smell is deliberate — a smell is a hint that invites a closer look, not a rule that demands action. Sometimes the smell is fine in context. The skill is learning to notice the smell and then decide.
flowchart LR
S["Code smell<br/>(a hint, not a rule)"] --> J{"Worth it<br/>right now?"}
J -->|yes| R["Pick a matching<br/>refactoring"]
J -->|no| N["Note it,<br/>move on"]
R --> T["Small step → test → commit"] A catalog overview
Section titled “A catalog overview”Here are the smells you will meet most often, each paired with the refactorings this course teaches to address it. The names come from the catalogue in Fowler’s Refactoring (2nd ed., 2018), chapter 3, “Bad Smells in Code”, written with Kent Beck. Treat this as your map: when something feels wrong, name the smell, then look up the move.
| Smell | What you notice | Where this course addresses it |
|---|---|---|
| Long Function | A function that scrolls off the screen and does many things | Composing Methods — Extract Function |
| Duplicated Code | The same fragment appears in two or more places | Extract Function and Moving Features |
| Mysterious Name | A variable, function or field whose name hides its purpose | Rename Function/Variable, Rename Field, Extract Variable |
| Large Class | A class hoarding too many fields and responsibilities | Moving Features |
| Long Parameter List | A call with so many arguments you lose track | Simplifying APIs |
| Feature Envy | A method more interested in another object’s data than its own | Moving Features |
| Primitive Obsession | Bare strings and numbers standing in for real concepts | Organizing Data |
| Shotgun Surgery | One small change forces edits scattered across many files | Moving Features |
| Repeated Switches | The same switch or nested branching repeated until it hides the logic | Simplifying Conditionals |
| Refused Bequest / Temporary Field | A subclass that disowns what it inherits, or state used only sometimes | Generalization & Inheritance |
A short example smell
Section titled “A short example smell”Here is Primitive Obsession: a raw number is passed around to mean “money,” but nothing stops a caller from mixing it up with a quantity or a temperature. The smell is the bare primitive standing in for a domain concept. The cure — wrapping it in a small type — lives in Organizing Data, but you should learn to smell it now.
// Smell: a bare number means "money" — easy to misusefunction total(price: number, shipping: number): number { return price + shipping;}
// Hint of the cure: give money its own typetype Money = { cents: number };
function totalMoney(price: Money, shipping: Money): Money { return { cents: price.cents + shipping.cents };}# Smell: a bare number means "money" — easy to misusedef total(price, shipping): return price + shipping
# Hint of the cure: give money its own typefrom dataclasses import dataclass
@dataclassclass Money: cents: int
def total_money(price: Money, shipping: Money) -> Money: return Money(price.cents + shipping.cents)// Smell: a bare number means "money" — easy to misusefunc Total(price, shipping float64) float64 { return price + shipping}
// Hint of the cure: give money its own typetype Money struct{ Cents int }
func TotalMoney(price, shipping Money) Money { return Money{Cents: price.Cents + shipping.Cents}}// Smell: a bare number means "money" — easy to misusefn total(price: f64, shipping: f64) -> f64 { price + shipping}
// Hint of the cure: give money its own typestruct Money { cents: i64,}
fn total_money(price: Money, shipping: Money) -> Money { Money { cents: price.cents + shipping.cents }}The wrapped version makes it impossible to accidentally add money to a quantity — the compiler or the type now carries the meaning that the bare number left to chance.