~ / track I / applied patterns
Indirection vs abstraction
IntermediatepatternBefore reaching for any "best practice," you need one distinction clear, because it decides whether a change helps or hurts: indirection versus abstraction.
Indirection is any detour that makes code harder to follow — a jump to another function, file, or layer to understand what happens here. Every abstraction creates an indirection. But not every indirection is an abstraction. An abstraction earns its detour by letting you ignore what's underneath and by generalizing over many cases; mere indirection just relocates code without hiding anything general.
The rest of this lesson walks one example from no indirection, to indirection, to a genuine abstraction — and counts the cost at each step. It pairs with Matchers, which builds directly on the abstraction we land on here.
#1 — No indirection
A function that enrolls someone in a course, with two rules inline: the applicant's level must clear the course minimum, and their party must fit the seats left.
There is no detour: every rule is right here. To understand enrollment you read exactly one function.
#2 — Indirection
Extract the rules into their own predicate:
This is indirection, not abstraction. enroll reads more cleanly, which is a
real gain. But nothing is hidden in a reusable way — the detour just moved the
rules one hop away. And it bought a cost: coupling. Add a third rule that
needs data valid-enrollment? doesn't currently receive, and you must change
both functions — the predicate's signature and the caller.
#3 — Abstraction
Look again at what the two rules have in common: each takes a value from the
incoming args, compares it against a value on the course, with some
comparator. That shape is itself data:
Now there's an abstraction. valid? doesn't know what a course is — it never
mentions :level or :seats-left. It takes a list of rules (a data structure
describing how to validate) and interprets them. The same valid? validates
orders, users, anything, against any rules you hand it. Adding a rule is now
adding data, not editing code.
The cost is real
An abstraction is not free, and it is not automatically better than the indirection it replaces:
- Positive:
valid?extends effortlessly to new entities and new rules of the same shape. - Negative: it resists rules that don't fit its shape. A rule comparing
two fields of the
courseto each other, or one needing a custom message per failure, is now harder to express than it was inenroll-v1. That is complexity — and every abstraction adds some. A poorly chosen abstraction can be more harmful than none at all, because it makes the cases it didn't anticipate expensive forever.
So there's no rule that abstraction beats indirection, or that zero indirection beats both. Overusing any of them hurts. The engineering work is choosing the right one for this problem — which means understanding the problem first.
On dogma
This is why blanket slogans — KISS, SOLID, "always extract" — make poor guides: they try to systematize something that genuinely depends on context. Design patterns are more useful, not as rules to apply everywhere, but as references for problems you've recognized. Understand the problem deeply; only then design the solution. The solution should never arrive before that understanding.
For the data-as-rules move generalized, see Data-oriented programming; for how a small, general surface keeps a library composable, see Designing libraries that compose; and for the combinators that make abstractions compose, see Composition and point-free style.
Check yourself
? quiz
You extract a one-line helper that's called from exactly one place and shares all the same data with its caller. What have you most likely created?
Exercise
The payoff of the valid? abstraction is that a new rule is just data. Prove it:
add a third rule without writing a new function or touching valid? — an
under-age applicant (:age below the course's :min-age) must be rejected. Edit
enrollment-rules in the Repl below; run until every assert passes.
Reference solution
(def enrollment-rules
[{:name :level-ok? :arg :level :key :min-level :comparator >=}
{:name :seats-ok? :arg :party-size :key :seats-left :comparator <=}
{:name :age-ok? :arg :age :key :min-age :comparator >=}])Not one line of valid? changed — that's the abstraction earning its cost.