As Dijkstra put it in the '60s: we start with hardware, which is fully capable of solving our problem, but not designed to make it convenient to express that solution.
So we make "virtual machines" by creating new "instructions" that extend the physical machine. (Dijkstra called them virtual machines; these days we might say APIs or something.)
We keep doing this: extending level n-2 into level n-1, with the guiding principle that n-1 should be a "virtual machine" that makes it slightly more convenient to express level n. At some level k, the solution to our original problem is trivial.
Other important properties of these abstraction levels is that
- They should do resource allocation and management to the point where a raw resource used by level n should not be visible as such in level n+1.
- Any level should ideally only depend on one level below it.
- No level can ever depend on a level above it -- this creates cycles in the dependency tree and prevents further extension and contraction of functionality.
----
Drawing further from Parnas instead: abstractions should hide design decisions that are likely to change. Typical examples of those are the format of data, layout of data structures, implementation details.
Things that are less likely to change are things that come from the problem domain. Design interfaces based on problem domain concepts, not solution domain concepts. This applies at every level: the abstractions of level n-1 should have interfaces in terms of problem domain concepts of level n.