An intuition for abstraction

Building an intuition around abstraction in code can take a little bit of practice and experience to learn. Let's make it easier.

In South Africa, you need to renew your driver’s license card every 5 years. This is a rite of passage – it involves forms, long queues, confusion, chaos, and eventually a new license card.

Here’s how it works:

Building intuition

Let’s notice a few things:

  1. The eye inspection person doesn’t know (or even care ) For all they care, you could’ve barged in, jumped to the front of the queue, and fought off a seagull, and then arrived in their office. about how, when, where, you filled out the form or what queue you even stood in. The only thing they care about is whether you have the right form, and whether you’re there for your eye test.
  2. Similarly, the teller doesn’t know (or care!) about who did your eye test, where it was done, where you filled out your form, or which queue you stood in. All they care about is whether you 1) have your filled out form, 2) have a valid eye test, 3) need to pay.

Also notice the following:

  1. The security guard does need to know which form to fill out and which queue you need to stand in.
  2. The eye test person only needs to produce a pass / fail result according to their own internal logic. It is not aware of the other steps in the process.
  3. Depending on whether you passed / failed your eye test, the clerk does need to know what the next step in the process is (pass -> go pay)

Generalizing, we can see that outer processes are aware of the sequence of events, as well as the required inputs to each function, but the functions themselves are unaware of both the previous and next steps:

This should be somewhat intuitive, if you place yourself in the shoes of any of the people involved in this process.

Making it concrete

Each person performs a function:

with various inputs:

But notice that, if there is coupling, it is directional:

In technical terms, we could say:

An external layer may depend on internal layers, but not the other way around.

This is one of the big ideas behind clean architecture (minus the confusing jargon like adapters, gateways, repositories, etc.).

The eye test person doesn’t need to know which queue you stood in to do their job! Just that you have a form, and you need an eye test .It would be bizarre if they said to you, “I understand that you need an eye test, and you have a form – but did you stand in queue A? Oh, you stood in queue B? No, that’s wrong – go stand in queue B! I’ll see you for the [exact same] eye test when you walk back in here after waiting in queue B.

The teller doesn’t need to know anything other than you have a valid form, valid eye test result, and are ready to pay.

The core idea is simple: Code shouldn’t care about what happens upstream before it is called. It should just accept the inputs, and execute, and if necessary be aware of how to encode the result when moving one layer deeper.

If returning from one level deeper, the outer layer should again be responsible for converting the inner layer’s output into the right abstraction again (for example, the security guard might be aware of where you should go next once you have the eye test and filled out form).

How this maps to writing software

Contrary to popular belief, writing software is itself an exercise in problem discovery. You know where you want to go, but it’s not immediately obvious how to get there . People will try and tell you to plan ahead, and you can do this to a certain degree, but knowing all unknowns ahead of time is unfortunately impossible for anything beyond a trivial change. The second you begin making changes, you’ll immediately notice that what you need to actually do is different from what you originally planned.

Using our traffic department example: you know that people will need to fill out a form, get their eyes tested, and pay. But it might not immediately be clear how to organize the various steps.

Maybe your initial implementation has one person do everything (a case of overloading), or maybe you unintentionally implement a security guard that also does the eye test (a case of failure to separate concerns), or maybe things are just difficult to test in isolation (another kind of smell for when your abstraction is wonky).

But fall back to asking yourself, “which component needs to be aware of the other? Where are the boundaries?” and then arrange your code (and abstractions) in a way that makes things cleanly isolated. You’ll find that, with some practice and experimentation, that the appropriate outer and inner layers naturally reveal themselves.

A good rule-of-thumb for this is that components that are most likely to change frequently are typically best placed on an outer layer.

For example, we may introduce more queues for people to stand in over time, or multiple security guard’s, or different reasons for being at the traffic department. These are things that are much more likely to change than for example requiring an eye test (which has been a requirement for decades). As a result, this is a strong tell that security guard’s role should be in an outer layer of the abstraction rather than an inner one (imagine the teller having to keep track of all of this?!).

The end

This hopefully helps build some natural intuition around what good abstraction feels like. You can peer inwards (or towards what comes next), but not outwards.

The next time you’re thinking about how to wire up your code and how to handle dependencies and abstractions, maybe you’ll remember to think of the South African traffic department πŸ‡ΏπŸ‡¦.