~ / track I / applied patterns

Indirection vs abstraction

Intermediatepattern

Before 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.

loading sci
press ⌘/Ctrl-↵ or click ▶ run to evaluate

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:

loading sci
press ⌘/Ctrl-↵ or click ▶ run to evaluate

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:

loading sci
press ⌘/Ctrl-↵ or click ▶ run to evaluate

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 course to each other, or one needing a custom message per failure, is now harder to express than it was in enroll-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.

loading sci
press ⌘/Ctrl-↵ or click ▶ run to evaluate
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.

 status: new