I wish articles like this had more examples in them. In between “this thin wrapper adds no value but a lot of complexity”, and “this thin wrapper clarified the interface and demonstrably saved loads of work last time requirements changed” is an awful lot of grey area and nuance.
I did like the advice that if you peak under the abstraction a lot, it’s probably a bad one, tho even this I feel could use some nuance. I think if you need to change things in lots of places that’s a sign of a bad abstraction. If there is some tricky bit of complexity with changing requirements, you might find yourself “peeking under the hood” a lot. How could it be otherwise? But if you find yourself only debugging the one piece of code that handles the trickiness, and building up an isolated test for that bit of code, well, that sounds like you built a wonderful abstraction despite it being peaked at quite a bit.
The article did start off giving TCP as a good abstraction but then didn't follow up with examples of bad abstractions.
Dynamic typing is an example of an indirection masquerading as an abstraction. You end up carrying around an object and occasionally asking it whether it's an int64_t or a banana. You maybe think your type luggage will take you on exotic vacations when really in fact you take it on exotic vacations.
To me, it ties in with John Ousterhout's concept of "deep, small interfaces"
TCP is a good abstraction because it's essentially 4 operations (connect, disconnect, send, receive), but there's a lot going on inside to make these operations work. So are TLS, filesystems, optimizing compilers and JITs, modern CPUs, React (or rather the concept of "reactive UI" in general), autograd and so on.
Articles like this are a dime a dozen. Literally, there are 1000s of articles that all say the exact same thing using way too many words: "Bad abstractions are bad, good abstractions are good".
I second this, such posts are very generic, they are hard to disagree with, but also to agree with empathically as there are no clear examples of what is too much.
As someone who uses lots of layers and dependency injection I would like to be poked on where is that too much abstraction but I end up being no wiser.
Relying on mere “taste” is bad engineering. Engineers do need experience to make good decisions, yes. But surely we are able to come up with objective criteria of what makes a good abstraction vs. a bad abstraction. There will be trade-offs, as depending on context, some criteria will be more important than other (opposing) criteria. These are sometimes called “forces”. Experience is what leads an engineer in assessing and weighing the different present forces in the concrete situation.
That’s seems like it should be true, and it would be great if it was.
But in my many years of experience working with Jr engineers, I have found no substitute other then practice guided by someone more Sr (who has good taste).
There are just too many different situations and edge cases. Everything is situational. You can come up with lists of factors to consider (better versions of this post often have them), but no real firm rules.
I wouldn’t call that “taste”. It’s not a matter of taste which solution is better. If different engineers disagree about which solution to choose, then it’s fundamentally a different assessment of the relevant factors, and not about taste. Or at least, it shouldn’t be the latter.
I don’t know. We could look for some other word to encode “often sub-conscious though sometimes explicit heuristics developed by long periods of experiencing the consequences of specific trade-offs” but “taste” seems like a pretty good one because it’s quite intuitive.
There often - usually? - are more than one good solution and more than one path to success, and I don’t find calling different good engineers making different choices primarily because of their past experiences an egregious misuse of language.
I think you are going after something that is more an element of craftsmanship than engineering, and I agree it is a big part of real world software development. And, not everyone practices it the same way! It's more of a gestalt perception and thinking process, and that instinctual aspect is colored by culture and aesthetics.
In my career, I've always felt uncomfortable with people conflating software development with engineering. I think software has other humans as the audience more so than traditional engineered products. Partly this may be the complexity of software systems, but partly it is how software gets modified and reused. There isn't the same distinction between the design and the product as in other domains.
Other domains have instances of a design and often the design is tweaked and customized for each instance for larger, complex products. And, there is a limited service life during which that instance undergoes maintenance, possible refurbishing, etc. Software gets reused and reformed in ways that would make traditional engineers panic at all the uncertainties. E.g. they would rather scrap and rebuild, and rely on specialists to figure out how to safely recycle basic materials. They don't just add more and more complexity to an old building, bridge, airplane, etc.
Yes, this is what I mentioned in my original comment about experience being needed to weigh the trade-offs. That doesn’t mean that we can’t very concretely speak about the objective factors in play for any given decision. We can objectively say that x, y, z are good about this abstraction and a, b, c are bad, and then discuss which might outweigh the other in the specific context.
Needing experience to regularly make good decisions doesn’t mean that an article explaining the important factors in deciding about an abstraction is useless.
If we used the word "judgement", would that be a better option?
It seems that pretty much anyone can write code (even AI), but ultimately in software development, we get paid for judgement.
Taste amounts to a well trained neural net in the engineers skull. It should not be belittled. Articles like this attempt to describe taste systematically, which is worth attempting but impossible
Maybe not, but you can still move the needle one way or another based on reading an article. For those readers who recognize themselves as erring on the side of adding too many abstractions, they might move the needle a bit towards the other side.
try using LangChain and you'll get countless examples of bad abstractions
started working with it this week for a new project
gosh, it's so painful and unintuitive... I find myself digging deep into their code multiple times a day to understand how I'm supposed to use their interfaces
"I wish articles like this had more examples in them."
There is a class of things that don't fit in blogs very well, because any example that fits in a blog must be broken some other way to fit into a blog, and then you just get a whole bunch of comments about how the example isn't right because of this and that and the other thing.
It's also a problem because the utility of an abstraction depends on the context. Let me give an example. Let us suppose you have some bespoke appliance and you need to provide the ability for your customer to back things up off of it.
You can write a glorious backup framework capable of backing up multiple different kinds of things. It enforces validity checks, slots everything nicely into a .zip file, handles streaming out the backup so you don't have to generate everything on disk, has metadata for independent versions for all the components and the ability to declare how to "upgrade" old components (and maybe even downgrade them), support for independent testing of each component, and has every other bell and whistle you can think of. It's based on inheritance OO and so you subclass a template class to fill out the individual bit and it comes with a hierarchy pre-built for things like "execute this program and take the output as backup" and an entire branch for SQL stuff, and so on.
Is this a good abstraction?
To which the answer is, insufficient information.
If the appliance has two things to backup, like, a small SQL database and a few dozen kilobytes of some other files, such that the streaming is never useful because it never exceeds a couple of megabytes, this is an atrocious backup abstraction. If you have good reason to believe it's not likely to ever be much more than that, just write straight-line code that says what to do and does it. Jamming that into the aforementioned abstraction is a terrible thing, turning straight code into a maze of indirection and implicit resolution and a whole bunch of code that nobody is going to want to learn about or touch.
On the other hand, if you've got a dozen things to backup, and every few months another one is added, sometimes one is removed, you have meaningful version revs on the components, you're backing up a quantity of data that perhaps isn't practical to have entirely in memory or entirely on disk before shipping it out, if you're using all that capability... then it's a fantastic abstraction. Technically, it's still a lot of indirection and implicit resolution, but now, compared to "straight line" code that tries to do all of this in a hypothetical big pile of spaghetti, with redundancies, idiosyncracies of various implementations, etc., it's a huge net gain.
I don't know that there's a lot of abstractions in the world that are simply bad. Yeah, some, because not everything is good. But I think they are greatly outnumbered by places where people use rather powerful, massive abstractions meant to do dozens or hundreds of things, for two things. Or one thing. Or in the worst case, for no things at all, simply because it's "best practices" to put this particular abstraction in, or it came with the skeleton and was never removed, or something.
I did like the advice that if you peak under the abstraction a lot, it’s probably a bad one, tho even this I feel could use some nuance. I think if you need to change things in lots of places that’s a sign of a bad abstraction. If there is some tricky bit of complexity with changing requirements, you might find yourself “peeking under the hood” a lot. How could it be otherwise? But if you find yourself only debugging the one piece of code that handles the trickiness, and building up an isolated test for that bit of code, well, that sounds like you built a wonderful abstraction despite it being peaked at quite a bit.