5.3 KiB
| type | title | summary | tags | scope | last_updated | ||
|---|---|---|---|---|---|---|---|
| convention | Sandi Metz Code Philosophy | Distills Sandi Metz's POODR and 99 Bottles principles into actionable rules for OO design. Answers "how should I structure this code, and when is abstraction appropriate?" |
|
global | 2026-06-27 |
Sandi Metz Code Philosophy
A philosophy of designing for change, not for cleverness. The goal is code that is easy to change later — which requires keeping objects isolated, messages explicit, and abstractions earned.
Core Principles
1. Make the change easy, then make the easy change. Before adding behavior, ask whether the current structure makes the change easy. If not, restructure first. This separates the two kinds of work and prevents mixed-purpose commits that are hard to reason about.
2. Duplication is cheaper than the wrong abstraction. Only extract when you have three real, concrete examples and can name the concept precisely. A vague name is a signal the abstraction isn't ready. Wrong abstractions attract more wrong code; tolerate duplication until the pattern is genuinely clear.
3. Every method/function does one thing. If you need "and" to describe what it does, split it. Metz's rule: ≤5 lines in Ruby. Adapted for other languages: ≤10 lines in JS. Single-purpose methods compose cleanly; multi-purpose methods resist change.
4. Classes have one reason to change (Single Responsibility). A class that changes for two different reasons is two responsibilities tangled together. Separate them so a change in one domain can't break an unrelated feature.
5. Design messages first, classes second. Objects are defined by the messages they respond to, not the data they hold. Start by deciding what message you need to send and what you need back — the class structure follows from that.
6. Open for extension, closed for modification.
if/switch/case chains that dispatch on type or ID violate this: each new case requires editing existing code. Replace with a registry (plain hash/object) so adding a case is data, not code change.
7. Depend on abstractions, not concretions (Dependency Injection). Inject collaborators explicitly rather than instantiating them inside methods. This makes tests trivial (pass a fake) and keeps modules decoupled from each other's concrete implementations.
The Enumerated Rules (Sandi Metz's Actual Rules)
These are the four specific limits Metz published — meant to be followed strictly until you fully internalize why:
- Classes: ≤100 lines
- Methods: ≤5 lines (Ruby; adapt proportionally to language)
- Method parameters: ≤4 (and prefer keyword/named args)
- Controllers: instantiate only one object
Breaking any of these is a smell worth investigating, not a veto — but the burden of proof is on the break.
Patterns
Shameless Green first. When starting a new feature, get to passing tests as fast as possible. Duplication is explicitly acceptable at this stage. Refactor after green, not before — premature abstraction when the shape is uncertain locks in the wrong structure.
Flocking Rules for refactoring. When pulling duplication into an abstraction: (1) find the most similar things, (2) find the smallest difference, (3) remove only that difference. One transformation at a time; stay green throughout. Big leaps break the test-feedback loop and introduce risk.
Composition over inheritance. Prefer has-a to is-a. Inheritance locks two classes into a hierarchy that's expensive to exit. Composition keeps the relationship explicit and replaceable.
Law of Demeter: only talk to immediate neighbors. a.b.c.d is a tell — you're coupled to the entire chain. Ask the immediate collaborator for what you need; let it traverse its own graph.
Anti-Patterns
- Class or method name has "And" or "Manager" → multiple responsibilities; split it
obj.foo.bar.bazchains → Law of Demeter violation; you're coupled to internal structurecase/ifswitching on type or id → Open/Closed violation; use a registry- Extracting an abstraction from one or two examples → wrong abstraction in the making
- Instantiating dependencies inside methods → untestable; inject instead
- Silent catch blocks or swallowed errors → violates single responsibility of error handling
- Inheriting from a class just to share code → composition violation; extract a module/mixin or shared object instead
- Subtype that changes exceptions or return types → Liskov Substitution violation; callers can't safely substitute
Exceptions
Language/runtime adaptation. The 5-line rule is for Ruby's expressive syntax. In JS/TS, 10 lines is a reasonable target. In Go with explicit error returns, a stricter reading breaks everything. Apply the spirit (single purpose, fits on a screen) proportionally.
Shameless Green is time-limited. Duplication is acceptable while finding the shape. Once you have three examples and a name, the refactor is overdue.
Performance-critical paths. Clean composition sometimes adds allocation or indirection at hot paths. Profile first; don't preemptively break good design for speculative speed.
Pragmatic team calibration. If your team hasn't read POODR, enforce the enumerated rules mechanically first — they create the right pressure without requiring philosophical buy-in upfront.