Concept · Code design
Code smells
Surface signs — long functions, data clumps, feature envy, shotgun surgery — that hint at a deeper design problem worth refactoring.
A code smell, a term coined by Kent Beck and popularised in Martin Fowler's Refactoring, is a surface feature of code that usually indicates a deeper problem. It's a prompt to look closer, not a rule: sometimes a long function is the clearest way to express a long algorithm.
The smells you'll meet most
| Smell | Looks like | Usual refactoring |
|---|---|---|
| Long function | Comments labelling sections; scrolling to read | Extract function |
| Long parameter list | f(a, b, c, d, e, true) | Introduce parameter object |
| Flag argument | A boolean that switches behaviour | Split into two functions |
| Duplicated knowledge | The same rule in several places | Extract and give it one owner (DRY) |
| Data clumps | The same fields always travel together | Extract a type |
| Primitive obsession | Money as float64, IDs and emails as bare strings | Introduce a value object |
| Feature envy | A method uses another type's data more than its own | Move the method |
| Message chains | a.B().C().D() | Hide the delegate (Law of Demeter) |
| Shotgun surgery | One change edits many files | Move related code together |
| Divergent change | One file changes for many unrelated reasons | Split by responsibility |
| Repeated switch on type | The same switch kind in several places | Replace with polymorphism |
| Speculative generality | Hooks, parameters and interfaces nobody uses | Inline or delete (YAGNI) |
| Dead code | Unreachable branches, unused functions | Delete it |
| Comments explaining confusing code | // this is tricky because... | Rename and restructure until the comment is unnecessary |
Many smells are coupling and cohesion problems seen from a different angle: shotgun surgery is low cohesion, feature envy is behaviour living away from its data.
Responding to a smell
- Confirm it matters. Is this code changed often? Is it causing bugs? A smell in code nobody touches can wait.
- Get it under test — characterization tests if necessary.
- Refactor in small, behaviour-preserving steps, running tests after each one.
- Keep the refactor separate from feature or bug-fix commits.
Linters catch some smells mechanically — long functions, too many parameters, cyclomatic complexity, unused code (golangci-lint in Go, ESLint in TypeScript). The rest need a reviewer's judgment. The refactoring lesson works through several in Go.