80 lines
5.3 KiB
Markdown
80 lines
5.3 KiB
Markdown
|
|
---
|
||
|
|
type: convention
|
||
|
|
title: Sandi Metz Code Philosophy
|
||
|
|
summary: 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?"
|
||
|
|
tags:
|
||
|
|
- type/convention
|
||
|
|
- domain/software-design
|
||
|
|
scope: global
|
||
|
|
last_updated: 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:
|
||
|
|
|
||
|
|
1. Classes: ≤100 lines
|
||
|
|
2. Methods: ≤5 lines (Ruby; adapt proportionally to language)
|
||
|
|
3. Method parameters: ≤4 (and prefer keyword/named args)
|
||
|
|
4. 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.baz` chains** → Law of Demeter violation; you're coupled to internal structure
|
||
|
|
- **`case`/`if` switching 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.
|