Nguyen Le PhongNguyen Le Phong

Programming Abstractions Are Bets

Every abstraction is a bet: win, and changes stack easily on top; lose, and you own a premature abstraction where every layered change comes out smelly. The satisfaction of naming things is real — which is exactly what makes it dangerous. It is always easier to generalize specialized code than to untangle an over-abstracted design.

There is a cliché that everything is a trade-off, and abstraction is no exception. The most common way to reach for one in OOP design is to abstract anything that can be named: “I’ll need to send a message — let’s introduce a message service; oh, also a message factory; also a message dispatcher; and how about…” — on and on. Sometimes it works beautifully. Sometimes it quietly brings more downside than it is worth.

Every abstraction is a bet

If you win, laying further changes on top of it becomes easy and fast. If you lose, you now own a premature abstraction, and every change layered on top comes out smelly. Eventually, as changes pile on changes, it starts to feel like a Jenga game where everyone already knows how it ends.

So abstractions are not free. It feels good to name things, to apply a freshly learned design pattern — that satisfaction is real, which is exactly what makes it dangerous. It is gambling, and more often than not the odds lean toward the losing side. The more convoluted the software already is, the smaller your winning fraction gets.

The comprehension cost

No matter how semantically beautiful an abstraction’s name is, it is probably harder to understand than its direct equivalent. Every intermediate layer a reader must pass through drains a little more cognition — especially readers who don’t share your intimate familiarity with the implementation. And “readers” may well include you, a couple of months from now, staring at your own cleverness without the context that once made it obvious.

This is what K.I.S.S. is really about: keeping things stupidly direct, so anyone can Ctrl/Cmd+Click and simulate how the runtime would unfold — without holding a tower of indirection in their head.

Remember you can always come back and abstract later, if the feature ever evolves to need it. And by then you will understand the problem better, having been through a few reality checks with the current code. It is always easier to refactor specialized code toward generality than to go the other way — untangling an over-engineered design with dependants already built on top of it.

A codebase is a living thing, with its own lifecycle and way of evolving; it is not a product delivered once and for all. Keep the wrong-abstraction risk and the comprehension cost in mind when justifying design decisions — yours or others’ — and when reviewing code. (A line of thinking sharpened for me in a friend’s garden of thoughts.)

Further reading

你觉得怎么样?