- Damian Płaza
Imagine you went on walking to do some small shopping because you needed edible goods.
It turned out that you got so many little things that you couldn't hold them all in your hands.
Fortunately, there were some plastic/paper bags you could use to solve this problem.
The next day, you wanted to return some IT books to your friend, but it wasn't safe to carry them outdoor. Snow and rain can easily damage precious knowledge available in the books.
To keep them away from moisture, you put them inside of the same bag you got your groceries earlier.
Next time, you needed to go to drive to a shop distant from your current place.
You went to your car, started the engine and the sound you have heard was far from the regular one.
You went out and lifted out the hood of the car - the hot steam was miserably unfriendly feedback from the reality that your car broke.
Everything was simple before this situation - use the key (or button depending if the car is "keyless") and drive happily to destination at will.
Unintentionally, internals got exposed to you - there was no need to see them before (or at least you didn't think of them).
What is the purpose?
I can hear your voice (at least I am simulating you were telling so): "What the hell are you talking about? Bags? Broken car?"
Two totally unrelated contexts, although both scenarios have something in common.
Those examples were made purposefully so I know the answer: both subjects of our little sceneries took away the details.
Of course, there are other reasons why things are as they are, but either a bag or a car provides a specific contract, when satisfied, gives back a certain function.
"(...) took away the details", but what are those "details"?
The details of the provided function.
Taking away the items
Coincidentally, we are talking about something which is really fundamental in the universe - abstraction.
Without abstraction, we are not able to operate, because of drowning in the ocean of details, nitty gritties and edge cases.
We want simplicity (and predictability, but that's a different story we will tackle at some point in the future).
Even though our examples have something in common, they also differ in one particular perspective.
We can carry various things in a bag - books, groceries, rocks, clothes and whatever else you can pick.
A bag abstracts away the details of the specific fabric endurance, ergonomics of handlers, and so on, simultaneously providing a handy interface to work with (in most cases).
I can imagine (or at least that's my reasoning) that a shopping carry bag was introduced to help customers taking back their goods.
The function provided by it is clearly orbiting around carrying, aggregating, holding together, etc. After putting of the stuff inside, we stop thinking of them as individual items, instead we refer to the "interface".
This unified contract opens a possibility to keep various types of things inside. When designed, this contract was supposed to be "generic" or "reusable".
On the other hand, the details kept under the hood of a car, were taken away for a "slightly" different purpose.
Taking away the complexity
They are taken away from our sight so that we can refer to a simple interface - "put a key, turn it over, go".
To fulfill this function a vast collaboration between sub-components need to happen. But we don't want to think about it all the time.
In this scenario, the main purpose was to primarily hide internals, and reusability of reasoning about it might be called a side-effect of this interface, not a purpose itself.
Additionally, it wasn't mentioned during the introduction of this tale, but each hidden part is an abstraction of other sub-parts. A consequence of such abstraction might be reusability, but yet again the details are hidden under a stable interface.
"Manufacturing the understanding"
We could build the conclusion that hiding complexity might lead to reusability, but no the other way around.
The reusable function might emerge when a given abstraction proves itself among various contexts.
So when the complexity, the internals, "the guts", are kept away from our sight, we start reasoning on a totally different level.
Our cognition flies high to the upper planes of understanding. We no longer "ignite the engine", but we do "put a key, turn it over, go".
Concluding, the process of abstracting can be thought of as a function that takes the details (with the related words, the "language") and outputs the intention-oriented, context-capturing concept.
It might be, coincidentally, that it can be reused in some other contexts, but then we need to check the language to prove its universality.
As we obtained the high-order concept, having a stable inteface, we can use its interface as a specification in building even higher-order functions (there is a great blog post called "Mocking as a design tool" written by Sandro Mancuso).
You ain't gonna need it so keep it simple, stupid
What about YAGNI? What about KISS? Isn't the "premature abstraction is the root of all evil"?
The question is about the purpose, the intention, "the why" - why are we abstracting?
If we want to drive the understanding by it - it's probably worth doing. Emphasizing the language, by keeping the details inside, was one of the topics of "Language of the problem" blog post.
Another trait, raising the probability of a decision for abstracting some details away, is "replaceability" - in order to have control over it when designing some other thing.
And "replaceability" leads to another interesting conclusion - we are able to grow, to evolve the structure, as we learn (check "Feature toggling as a design tool").
In fact, this is not anything new. It is the hallmark of modularity, testability and other properties of a well-designed (sub)system that the IT industry icons are speaking over and over.
At the same time, I'm thinking that YAGNI and KISS might be treated as really dangerous abbreviations (please note I don't say they are bad or wrong, but dangerous).
They are highly context-dependent, intention-hiding, distilled forms of a thought process and experience.
Actually, what does it mean "simple"? What does "the need", in YAGNI, refers to?
If working with all the low-level "gizmos", providing the universal interface full of getters and setters, means simple then we have a problem.
This might be a good example of the inherent property of "coding", which is totally opposite to "modeling". However, the problem with modeling is that it doesn't come without costs (you don't know what I am talking about? Please check "The cost of modeling").
Don't repeat yourself, stupid
Contradictory to YAGNI and KISS, we have DRY.
"Repeating" sounds like an another form of "reusability", and we already touched it a bit.
When the main goal is unification, in the form of a generic contract, we might be missing a point of abstraction and then we fall into "premature" or "too early" issue.
Wrapping a bunch of lines, that initially look as if they are building up a bigger responsibility, in a function might be a good idea unless the main intention is not reusability.
This needs to be proven, battle-tested.
One question to rule them all
There is one particular question that might reveal weaknesses of the design.
But it needs reflection and proper effort put into the process.
This question is: "how am I going to test it?" (please note that it doesn't exclude other questions and tools of analysis).
Uncontrollable dependencies, relatively high (what does it mean? 🤔) probability of changes, various cases and scenarios are some of the heuristics we can have.
Another interesting point is grouping the manifold of parts, according to a responsibility they need to jointly provide. Putting them "inside a box" opens a possibility to work only on this responsibility as a whole.
How do we know how many parts we need to have? We can assume, but it's probably better to discover them, hence the Outside-In TDD might be helpful here.
We have enormously powerful techniques in our tool belt - and one of them is abstracting away the details (of function, or implementation).
As I wrote in "The value of Value Objects", the value might be visible in building higher-levels of reasoning by using context-specific language. The value might not be directly in reusability (which might come as a side-effect).
We have discriminated unions, classes, modules, namespaces, functions, methods, REST APIs and other "programming" constructs to represent higher ways of reasoning so let's use them to express the meaning of our thought process and a snapshot of the state of mind.