Abstraction quality
Decide whether a proposed or existing abstraction — function, class, interface, module, generic, plugin hook — is worth having. Bad abstractions calcify. The cost of a wrong abstraction exceeds the cost of the duplication it replaces. This skill is a decision procedure, not philosophy.
First principles
An abstraction is worth having only if it hides substantially more complexity than its interface exposes. Ousterhout calls this depth. A shallow abstraction — where interface surface and implementation size are close — pays interface cost without delivering benefit.
Abstractions fail in two classic ways. Guard against both:
- Leaky (Joel Spolsky): the abstraction claims to hide a lower layer, but the layer breaks through at the margins — performance, error modes, semantics. All non-trivial abstractions leak to some degree; the sin is pretending they do not. Document the leaks.
- Wrong (Sandi Metz): the abstraction was extracted from apparent duplication that was actually coincidence. Over time, flags and conditionals accumulate as callers diverge. The fix is to go back: inline, delete per-caller dead branches, re-derive.
Decision procedure — "Should I extract this abstraction?"
Run in order. A single no means do not extract yet.
- Is the concept nameable in the domain? Can you name it with a precise noun or verb from the problem domain, without
Manager, Helper, Handler, Processor, Util, Wrapper, Base, Abstract*, *Impl, Info, Data? If not, the concept is not real yet.
- Are there at least three call sites that would all need the same change if the underlying concept changed (Rule of Three)? Two sites are usually coincidence. One is certainly too early.
- Do the call sites truly share behavior? If extracting requires new parameters or flags at each site to match each caller's needs, the sites are not the same concept. Stop.
- Is the interface smaller than the implementation? Count public parameters + method signatures; count non-trivial implementation lines. If the ratio is close to 1, you are about to create a shallow module. Redesign or keep duplication.
- Would inlining this abstraction produce clearly worse code? If inline looks equal or better, prefer inline.
- Is the underlying domain stable? If you are still learning the problem, duplicate. The right abstraction becomes visible only after you have seen the variations.
If all six pass, extract. If any fail, duplicate instead and tag with a one-line comment noting where the duplication lives, so you can revisit.
Decision procedure — "Is this existing abstraction good?"
Score each. Two or more failures → refactor candidate.
- Depth. Interface substantially smaller than implementation.
- Cohesion. One-sentence description without "and". If you need "it does X and Y", the module is two things wearing a trench coat.
- Honest leaks. Where it leaks (timing, error semantics, resource ownership), the leak is documented and predictable.
- Loud failure. When it breaks, callers get a typed error or a crash, not a plausible wrong answer.
- Stable signature. Changes to implementation rarely force signature changes. If every new requirement adds a parameter, the abstraction is misaligned with the real axis of variation.
- No flag parameters. Boolean flags that switch behavior are evidence of two concepts fused into one.
do(x, shouldValidate=true, exceptIfDraft=false) is the Wrong Abstraction in progress.
- Stable conditional count. Count
ifs keyed on parameters inside the abstraction. Growing over time ⇒ Metz decay.
- Reversible. You could inline it back without structural loss. An abstraction that cannot be inlined is either genuinely deep or badly entangled — inspect further.
The cost of a wrong abstraction, and how to escape it
The failure mode (Metz):
- A sees duplication, extracts a shared helper, replaces callers.
- A new requirement is almost like the helper. B, feeling honor-bound, adds a parameter and a conditional.
- Time passes. C, D, E each add flags. The helper now does several unrelated things, interleaved.
Escape:
- Inline the helper back into each call site.
- In each call site, set flag parameters to that caller's actual values and delete the unreachable branches.
- You now have concrete, possibly repetitive code at each site. Read them side by side. The true axes of variation are often different from what you originally extracted — or there are none.
- Re-extract only what passes the six-point procedure above.
The fastest way forward is back.
Against speculative generality
Do not build hooks for imagined future needs.
- No "we might need another provider someday." Until the second provider exists, a factory hides nothing and adds surface area. When the real second provider arrives, you will know its shape; guessing now is almost certainly wrong.
- No
IFoo per Foo. Create an interface only when ≥2 real implementations exist, or when you have a concrete seam the tests need that a simpler mechanism cannot provide.
- No configuration for things that never vary. Hard-code until variation becomes real.
- No generics you cannot fill with three concrete type instantiations. Generics express real polymorphism, not expected flexibility.
Fowler's cost ledger for unneeded generality: cost to build (wasted if unused), cost of delay (displaced a real feature), cost of carry (every reader must understand it), cost of repair (rip out when the guess turns out wrong). Four costs against a benefit that may never arrive.
Against naming-as-concealment
Util, Helper, Manager, Handler, Processor, Wrapper, Common, Base, Abstract*, *Impl are not concepts. They are confessions that the author could not find a name. Sometimes these names are appropriate because they are genuinely domain-accepted (e.g., HttpHandler in a web framework, EventHandler in a UI toolkit). Test: does the word appear in the user's domain vocabulary, or only in your code? If only in your code, it is an unhooked bag.
Locality vs DRY
Locality of Behavior (Carson Gross) and DRY (Hunt & Thomas) are in tension. DRY says "knowledge should have one representation." Locality says "code you need to understand a unit should be near that unit." When they conflict:
- DRY wins for true knowledge duplication — a business rule, a schema, an invariant. Change it in one place, safely.
- Locality wins for coincidental duplication — two functions that happen to have similar shapes but model different things — and for glue code (event bindings, markup, styles) where following a cross-file pointer hurts comprehension more than the duplication does.
- If unsure whether duplication is "true" or "coincidental," treat it as coincidental until evidence says otherwise. That defaults you to the Rule of Three plus the name test.
Note: DRY in Hunt & Thomas's original formulation is about knowledge, not text. Two functions with identical bodies that represent different business rules are not a DRY violation — they are a naming opportunity. Remember: different things should look different.
See also
references/decision-trees.md — flowcharts for extract / keep / inline decisions, with each branch labeled.
references/good-vs-bad-abstractions.md — paired TypeScript examples with commentary.
1---2name: abstraction-quality3description: Use when deciding whether to extract a function, introduce an interface, create a new class, add a helper, split a module, add a plugin hook, or generalize a piece of code; when reviewing a pull request that proposes a new abstraction; when a shared helper is growing flag parameters; when you have duplication and are unsure whether to extract it; or when an existing abstraction feels wrong but you cannot name why. Also trigger when the user asks "should I extract this?", "is this the right abstraction?", "is this over-engineered?", "is this DRY enough?", "should I generalize this?", "should I add an interface here?", or "is this premature abstraction?". This skill provides a decision procedure; it is referenced by writing-good-code and refactoring-and-reviewing-code, and can also be used on its own.4---56# Abstraction quality78Decide whether a proposed or existing abstraction — function, class, interface, module, generic, plugin hook — is worth having. Bad abstractions calcify. The cost of a wrong abstraction exceeds the cost of the duplication it replaces. This skill is a decision procedure, not philosophy.910## First principles1112An abstraction is worth having only if it **hides substantially more complexity than its interface exposes**. Ousterhout calls this *depth*. A shallow abstraction — where interface surface and implementation size are close — pays interface cost without delivering benefit.1314Abstractions fail in two classic ways. Guard against both:1516- **Leaky** (Joel Spolsky): the abstraction claims to hide a lower layer, but the layer breaks through at the margins — performance, error modes, semantics. All non-trivial abstractions leak to some degree; the sin is pretending they do not. Document the leaks.17- **Wrong** (Sandi Metz): the abstraction was extracted from apparent duplication that was actually coincidence. Over time, flags and conditionals accumulate as callers diverge. The fix is to **go back**: inline, delete per-caller dead branches, re-derive.1819## Decision procedure — "Should I extract this abstraction?"2021Run in order. A single **no** means do not extract yet.22231. **Is the concept nameable in the domain?** Can you name it with a precise noun or verb from the problem domain, *without* `Manager`, `Helper`, `Handler`, `Processor`, `Util`, `Wrapper`, `Base`, `Abstract*`, `*Impl`, `Info`, `Data`? If not, the concept is not real yet.242. **Are there at least three call sites** that would all need the same change if the underlying concept changed (Rule of Three)? Two sites are usually coincidence. One is certainly too early.253. **Do the call sites truly share behavior?** If extracting requires new parameters or flags at each site to match each caller's needs, the sites are not the same concept. Stop.264. **Is the interface smaller than the implementation?** Count public parameters + method signatures; count non-trivial implementation lines. If the ratio is close to 1, you are about to create a shallow module. Redesign or keep duplication.275. **Would inlining this abstraction produce clearly worse code?** If inline looks equal or better, prefer inline.286. **Is the underlying domain stable?** If you are still learning the problem, duplicate. The right abstraction becomes visible only after you have seen the variations.2930If all six pass, extract. If any fail, **duplicate instead** and tag with a one-line comment noting where the duplication lives, so you can revisit.3132## Decision procedure — "Is this existing abstraction good?"3334Score each. Two or more failures → refactor candidate.3536- **Depth.** Interface substantially smaller than implementation.37- **Cohesion.** One-sentence description without "and". If you need "it does X *and* Y", the module is two things wearing a trench coat.38- **Honest leaks.** Where it leaks (timing, error semantics, resource ownership), the leak is documented and predictable.39- **Loud failure.** When it breaks, callers get a typed error or a crash, not a plausible wrong answer.40- **Stable signature.** Changes to implementation rarely force signature changes. If every new requirement adds a parameter, the abstraction is misaligned with the real axis of variation.41- **No flag parameters.** Boolean flags that switch behavior are evidence of two concepts fused into one. `do(x, shouldValidate=true, exceptIfDraft=false)` is the Wrong Abstraction in progress.42- **Stable conditional count.** Count `if`s keyed on parameters inside the abstraction. Growing over time ⇒ Metz decay.43- **Reversible.** You could inline it back without structural loss. An abstraction that cannot be inlined is either genuinely deep or badly entangled — inspect further.4445## The cost of a wrong abstraction, and how to escape it4647The failure mode (Metz):48491. A sees duplication, extracts a shared helper, replaces callers.502. A new requirement is *almost* like the helper. B, feeling honor-bound, adds a parameter and a conditional.513. Time passes. C, D, E each add flags. The helper now does several unrelated things, interleaved.5253**Escape:**5455- Inline the helper back into each call site.56- In each call site, set flag parameters to that caller's actual values and **delete the unreachable branches**.57- You now have concrete, possibly repetitive code at each site. Read them side by side. The true axes of variation are often different from what you originally extracted — or there are none.58- Re-extract only what passes the six-point procedure above.5960The fastest way forward is back.6162## Against speculative generality6364Do not build hooks for imagined future needs.6566- **No "we might need another provider someday."** Until the second provider exists, a factory hides nothing and adds surface area. When the real second provider arrives, you will know its shape; guessing now is almost certainly wrong.67- **No `IFoo` per `Foo`.** Create an interface only when ≥2 real implementations exist, or when you have a concrete seam the tests need that a simpler mechanism cannot provide.68- **No configuration for things that never vary.** Hard-code until variation becomes real.69- **No generics you cannot fill with three concrete type instantiations.** Generics express real polymorphism, not expected flexibility.7071Fowler's cost ledger for unneeded generality: cost to build (wasted if unused), cost of delay (displaced a real feature), cost of carry (every reader must understand it), cost of repair (rip out when the guess turns out wrong). Four costs against a benefit that may never arrive.7273## Against naming-as-concealment7475`Util`, `Helper`, `Manager`, `Handler`, `Processor`, `Wrapper`, `Common`, `Base`, `Abstract*`, `*Impl` are not concepts. They are confessions that the author could not find a name. Sometimes these names are appropriate because they are genuinely domain-accepted (e.g., `HttpHandler` in a web framework, `EventHandler` in a UI toolkit). Test: does the word appear in the user's domain vocabulary, or only in your code? If only in your code, it is an unhooked bag.7677## Locality vs DRY7879Locality of Behavior (Carson Gross) and DRY (Hunt & Thomas) are in tension. DRY says "knowledge should have one representation." Locality says "code you need to understand a unit should be near that unit." When they conflict:8081- **DRY wins for true knowledge duplication** — a business rule, a schema, an invariant. Change it in one place, safely.82- **Locality wins for coincidental duplication** — two functions that happen to have similar shapes but model different things — and for glue code (event bindings, markup, styles) where following a cross-file pointer hurts comprehension more than the duplication does.83- If unsure whether duplication is "true" or "coincidental," treat it as coincidental until evidence says otherwise. That defaults you to the Rule of Three plus the name test.8485Note: DRY in Hunt & Thomas's original formulation is about *knowledge*, not text. Two functions with identical bodies that represent different business rules are not a DRY violation — they are a naming opportunity. Remember: different things should look different.8687## See also8889- `references/decision-trees.md` — flowcharts for extract / keep / inline decisions, with each branch labeled.90- `references/good-vs-bad-abstractions.md` — paired TypeScript examples with commentary.