3 ms·
While this is how the term is often used, I think it's cavalier use of language and confuses abstraction for modularity, and this linguistic confusion is one of
by voidhorse 2y ago
While this is how the term is often used, I think it's cavalier use of language and confuses abstraction for modularity, and this linguistic confusion is one of the reasons a lot of programmers write bad "abstractions".
Organizing your code into components that are as independent as possible is a good practice and is the pursuit of modularity. A proper abstraction on the other hand, is a generalization that simplifies a conceptual layer in your code base.
Abstraction often enables greater modularity as a consequence, but they are not the same thing. For example, in the problem of text editing, people eventually realized that the manipulation of text is typically line oriented. Thinking of a text file as a collection of lines may seem like an obvious and modest abstraction, but it works well. This abstraction, in turn, leads to other couplings (e.g. line oriented motion is highly dependent on the line abstraction), but it also leads to potential modularity (e.g. printer code may no longer need to understand exactly how a display renders each character of text in a grid, instead, it too can work on "lines"). Good abstractions support modularity to the extent that they establish a shared domain of objects to communicate about and across systems, but they do not necessarily produce modularity in themselves.
- danparsonson 2y agoYes, abstraction and modularity are coincident but distinct topics, and my last paragraph implies that I am equating those two things. Good point. I disagree that considering a text file as a set of lines really qualifies as an abstraction; it's just a different representation of the data - you could instead choose to use a list of words or a tree or whatever. An abstraction is a general interface to a concrete thing that allows you to substitute different but similar concrete things without changing the consumer of those things. The Linux filesystem is a great example - I can 'cat' basically anything that has a path, and some driver will pull data from the associated endpoint and display the data on screen. "File" is the abstraction, and consumers of files need not care about the specifics of communicating with the underlying devices. Such a system is also modular, but it's hard not to be when your abstraction is well conceived. Perhaps we're saying the same thing with different words?
- mannykannot 2y agoWhen you write "an abstraction is a general interface to a concrete thing that allows you to substitute different but similar concrete things without changing the consumer of those things", I feel you are actually describing a module created by making use of abstractions. Abstractions almost always represent data differently than as it is in its most concrete form (e.g. lists and trees are abstractions both often built of vectors and pointers), but that is not the purpose of abstraction. To continue with the original example, the representation of text as a sequence of lines is a sort of abstraction, but rarely the most useful one: words, sentences, paragraphs and/or syntax trees are often what you want, and they are useful because they give us a representation of the raw text that is aligned with its purpose, not because we can implement them in multiple ways. Furthermore, while we want to do some information hiding when implementing these abstractions (think particularly of syntax trees), that is not the purpose of having these abstractions - what we are hiding is the infrastructure to make it work, not any information conveyed by the text itself.
- usrusr 2y agoI don't like the mental image of layers. It's not wrong, but it's terribly inclusive of everything that is bad about bad abstractions. Even the worst abstractions will often look kind of nice from the layers angle. I prefer the concept of orthogonality: you have an entire plane where your domain requirements can move in, but the way you make persistence reliable is completely orthogonal to that plane. It should be implemented in a way that does not change when, say, your customer account becomes a composite of of multiple identities. And neither should the way you organize UI components change, or your transportation tooling change. That entire transportation layer stack, from ethernet all the way up to your domain model generator of choice. Did I say layers? Yeah, I did. You'll often see the orthogonal aspects themselves consisting of layers. But that's an implementation detail and those layers tend to be more pragmatic than dogmatic, as in the way TCP goes out of its way to avoid hiding any of the addressing of the underlying IP. TCP absolutely is a layer, but it has zero ambition to perhaps run on top of something other than IP, or hide differences between IPv4 and IPv6 from higher layers. It focuses on one thing, implementing streams on top of packets, again a problem nicely orthogonal to the problem of routing those packets (what IP does) or anything built on top of those streams.
- vacuity 2y agoTo add on to your great comment, I've read a few papers that justify your remarks, but some have paywalls and I'll try to condense them anyways. Parnas' criteria for when to split code into modules[0] is very compelling: modules should enforce information hiding, where the information of each module is how a key decision is implemented. Key decisions should be orthogonal: e.g., encryption shouldn't inform filesystem access and vice versa. This fits in nicely with practices for determining solutions for a given problem: nail down key decisions (or uncertainties around them) early on and encode them through modules and interfaces. A logical application of Parnas' module criteria is, naturally, in network stacks. According to [1], although coarse program flow generally follows the OSI layers, the fine control flow may hop around quite a bit. For some time people tried to keep the OSI layers as inspiration and use hacks like upcalls to selectively violate layering, which is a sure sign that the layers are unhelpful. Instead, modules should reflect functionality like encryption, NIC interfacing, and routing. Aim for a flatter, horizontal architecture like a B-tree, not a contrived vertical tower with hacks to cross layers "incorrectly". There will be hierachies and layers, but they should be obviously necessary now. Layering is not the end or even the means but merely how things are. A program's capability (functionality) is determined by the capabilities of its dependencies, and splitting up functionalities well is important. I use this language to relate it to capability-based security[2]. After all, "security" is basically just a stricter form of program "correctness", and just as a terminal emulator's input text handling capability shouldn't have much to do with its window event handling capability because malicious input shouldn't cause dangerous graphics bugs, there isn't much logical reason to have overlap regardless. They govern separate devices, protocols, and purposes. Capabilities along the lines of capability-based security reify the interfaces and provide strict enforcement. Lastly, flexibility around protocols should be added or discard as given by the precise problem at hand. Don't preemptively choose some extreme, but instead be informed by what is actually desired. It will yield substantial implementation complexity and performance detriments[3] to over-generalize, and of course over-specializing is just inadequate. [0] https://prl.khoury.northeastern.edu/img/p-tr-1971.pdf [1] https://ieeexplore.ieee.org/document/662046 [2] https://en.wikipedia.org/wiki/Capability-based_security [3] https://ieeexplore.ieee.org/document/208098