Programming is very complex because computers are very dumb. Computers need to be given precise and comprehensive instructions, which ends up being a lot of instructions, even for tasks we humans consider simple.
Since we’re slow-witted, the complexity of a program quickly overwhelms us intellectually. So, to deal with this, we programmers use mental tools to make a program’s complexity manageable as we build or maintain it.
The main tool we use for this is abstraction. Abstraction allows us to decompose an unworkably complex program into (a hierarchy of) modules of manageable complexity, where each module has one purpose (or maybe slightly more) and its complexity is reduced to what is relevant to achieving this purpose; all irrelevant complexity is hidden from the module. This is what abstraction constructs in programming do for us - they enable the “expression of relevant details and the suppression of irrelevant details” [a]
(Note that I’m not using “module” to refer to a particular language construct like JavaScript modules. I’m using it to refer to any chunk of code that is meant to achieve a particular purpose and that is a piece of a larger program.)
For example, say there’s a web page containing an HTML element with the ID "el-twenty"
and we want to set this element’s content to the number 20. The details or ideas that are directly relevant to achieving this are:
- The web page (an object)
- The element (an object)
- Getting the element from the web page by its ID (an operation)
- Setting the element’s content (an operation)
If we were to apply abstraction in solving this problem, we would end up with a certain module (let’s call it the root module) that expresses only these four relevant ideas.
This doesn’t happen naturally, however, because each of these ideas requires an implementation (a representation of the idea as a program). And an implementation is one or multiple new lower-level ideas. The implementation of idea c, for example, might be a particular search algorithm, whereas the implementation of idea a might include a tree data structure of elements. So the implementations of these four ideas will introduce new ideas that are irrelevant to the root module.
Abstraction, therefore, enables us to separate an idea from its implementation. It enables us to use ideas without regard for their implementation.
So we can have a root module that expresses only the four relevant ideas while hiding their implementation (the irrelevant ideas).
const element = document.getElementById("el-twenty")
element.innerText = 20
document
is idea a (the web page), the variable element
will hold idea b, the method getElementById
is idea c and the second statement—element.innerText = 20
—is idea d.
How easy is it to assess this module for correctness? How easy is it to understand and maintain it?—orders of magnitude easier than if the module included the entire implementation of the four ideas.
Today, a lot of ideas or abstractions (and their hidden implementation) are provided by frameworks, libraries and the platforms we build software for. If you know web dev basics, then you should have noticed that all the abstractions used in the snippet above are provided by any standard web browser. Web backend frameworks like Spring and Laravel provide abstractions for a client request, a data store, cache and many others things. There are libraries that provide abstractions for drawing graphics on a screen, rendering a geographic map, running predictions using machine learning models, etc.
Many, or maybe most, shared problems have a ready-made and accessible abstraction (on npm, maven, crates.io, etc). But the problems we solve have (or should have) idiosyncrasies that require us to build our own abstractions. Therefore building abstractions is still, as it was in Dijkstra’s day, a fundamental skill of software design.
But how do we build abstractions? Which programming constructs do we use for this? And which programming concepts should we use to do this well?
Stick around to find out… Or not… I don’t care.