Live data from Hacker News

The Cost of Abstraction (2016)

250bpm.com

41–50 of 65 posts

Re: The Cost of Abstraction (2016)

#41
post #34

Earlier quoted context omitted.

Or if you start with multiple wrapped dependencies from the start, the library is frequently updated and/or you touch the library in many places in your code across multiple projects especially, those are good places for wrappers/facades/adapters. An example would be in games when you submit an achievement or leaderboard score to a third party such as Apple GameCenter, Google Play Game Services, or other systems like…

Even if you have wrapped it you still have to update your code when there are breaking changes. If you follow "don't repeat yourself" (DRY) there will not really be more work updating, compared to if you had wrapped it. (You should however not follow DRY slavishly - but that is a discussion on it's own.)

If the wrapper/library is generic enough then it can be less work across projects and codebase and won't need to change with an actively updated library that is wrapped.

If the abstraction wrapper is tightly coupled, uses third party library types that can change or leaky then yeah, the effort is moot and you end up with more work.

Another case is where you have some sort of messaging system where you want responses/events to be uniform to your system rather than unique to a platform or even a company/product that doesn't match your codebase. An example might be wrapping a game recording library, audio library or something that does not fit well in the codebase or complicates maintenance, even stylistically/standards, or you only need small part of the library like activating it, common types messaging or data level.

Game engines themselves are massive wrappers around many wrapped systems.

Re: The Cost of Abstraction (2016)

#42
post #3

That's a really simply abstraction there, and I bet most people won't even call that an abstraction. It's a bit like saying bricks are useless, by limiting the entire argument to a single brick.

You are exactly right. It's indirection without abstraction. Good abstractions are simpler than the sum of their parts, the author's example of abstraction is clearly far more complicated (templates, references) than two increment operations. Good abstractions are ones you don't even think about, like arithmetic operators. How often do non-embedded/non-OS developers think about how multiplication is implemented?

Quoting from the blog post:

>It also seems that Go's implicit interfaces were designed to avoid unnecessary abstraction.

Actually, interfaces are often extremely abstract in Go. How much more abstract does it get than io.Reader? It's a thing that you can read bytes from into slices (arrays). The io.Reader abstraction is far simpler than os.File, net.Conn, or even bytes.Buffer (a file object, a network connection, and an in-memory buffer, respectively).

Re: The Cost of Abstraction (2016)

#43
These abstraction discussions seem to always result in commenters talking over each other. I would love to have these conversations rooted in a code sample. Otherwise no one is talking about the same thing.

If someone writes a blog series called “should this be abstracted, what’s the abstraction?” I think we would see some great discussion.

Re: The Cost of Abstraction (2016)

#44
post #6
post #3

That's a really simply abstraction there, and I bet most people won't even call that an abstraction. It's a bit like saying bricks are useless, by limiting the entire argument to a single brick.

It's almost like he's making a small non-real-life example to illustrate his general point, which is not confined to the particular example.

He shows an example of indirection without abstraction, then blames abstraction. His point isn't wrong, the problem is that he's confusing abstraction with any form of indirection. He claims that abstractions increase cognitive load when in fact the opposite is true. Bad abstractions and indirection without abstraction increase cognitive load.

I agree with him that abstractions should be crafted carefully, but not because abstractions are categorically bad. If he thinks abstraction is so bad, why does he write software at all? Software is extremely abstract, even assembly language is an abstraction of what the CPU does. Human language is abstract too: we can talk about trees without having to consider any specific tree.

Abstraction is a fundamental building block of human civilization. Of course, he doesn't actually believe that abstractions are bad. It would be nice if he differentiated between good abstraction and bad abstraction.

On a side note: I take issue with his criticism of mocking. Good abstractions are easily mocked and make the code base easier to understand because you don't have to consider every detail of the application at the same time. On the other hand, mocking concrete objects,[0] rather than abstract roles, definitely complicates things.

[0] Or "header interfaces" that duplicate the API of a concrete class exactly : https://www.martinfowler.com/bliki/HeaderInterface.html

Re: The Cost of Abstraction (2016)

#45
post #32
post #5

I've seen excessive abstraction kill projects time and time again. The crime scene is similar in every case: - What if we ever need to change database server or driver? Proceeds to write a layer to abstract data access - We might have different login forms one day? LoginFormFactory it is - This code hits our KV Redis/memcache/NoSQL by calling the driver lib directly? Can't have that, I'll write a CacheStorage or Docu…

To me the value of abstractjng out those services is testability - if they're abstracted, they can be mocked. The smaller the abstraction, the smaller the surface area you have to mock.

The smaller the abstraction, the more complex the emergent behaviour is when you combine all the itty-bitty little abstractions together, and the harder it is to write tests that cover real use cases instead of testing implementation details, which is another way of saying "your tests will be brittle".

You can band-aid this to an extent with higher-level "integration" tests, but if you'd done things at the right level of abstraction in the first place you would carry less weight around and wouldn't have to maintain a bunch of brittle tests in the first place.

This is obviously all shades of grey, but if you're mocking out things that aren't I/O you're probably doing it wrong.

If you find yourself vehemently disagreeing with this I'd be interested to know if you've ever had to refactor or simplify a codebase with a bunch of overly-abstracted, itty-bitty things that had very tight coupling via mock behaviour to all their tests, and if so, whether that felt pleasant to you or not. If you haven't then you probably haven't seen the considerable longterm maintenance downsides to this kind of approach and I feel sorry for the poor folk who will inherit your codebase.

Also curious is that many codebases I come across that look like this often have terrible copy/paste mock setup across lots of tests, making the issue even worse when you want to change things.

Those sorts of codebases often end up with people wrapping the abstractions in other abstractions because they're sufficiently resistant to change as a result that that seems easier. This obviously makes everything even more resistant to change (especially as the wrapper abstraction usually depends deeply on all the behaviour underneath it, and the mock set-up to test the wrapper ends up as an exercise in mentally mismodelling how the other components actually behave).

Re: The Cost of Abstraction (2016)

#46
post #5

I've seen excessive abstraction kill projects time and time again. The crime scene is similar in every case: - What if we ever need to change database server or driver? Proceeds to write a layer to abstract data access - We might have different login forms one day? LoginFormFactory it is - This code hits our KV Redis/memcache/NoSQL by calling the driver lib directly? Can't have that, I'll write a CacheStorage or Docu…

I confess I've never seen that happen, but I do think excessive abstraction is killing the argument this article is trying to make.

When would anyone ever make an inc_pair function? Or a hierarchy of 'animals that lay eggs'? I guess these are supposed to be examples of something, but I'm not sure what. I wouldn't even call them "abstraction". They seem like completely hypothetical examples of how to apply programming language features to non-problems in bad ways.

Without knowing what the designer of these programs is trying to model, it's impossible for me to understand what they're trying to accomplish or why they think applying these language features in this way makes sense at all.

Both of these examples sound like they increase the number of lines of code, for no appreciable benefit, and I recall Yegge's old observation that the main issue with any codebase is simply the bulk of it.

Re: The Cost of Abstraction (2016)

#47
post #32

Earlier quoted context omitted.

To me the value of abstractjng out those services is testability - if they're abstracted, they can be mocked. The smaller the abstraction, the smaller the surface area you have to mock.

The smaller the abstraction, the more complex the emergent behaviour is when you combine all the itty-bitty little abstractions together, and the harder it is to write tests that cover real use cases instead of testing implementation details, which is another way of saying "your tests will be brittle". You can band-aid this to an extent with higher-level "integration" tests, but if you'd done things at the right leve…

>The smaller the abstraction, the more complex the emergent behaviour is when you combine all the itty-bitty little abstractions together

That's not necessarily true. Abstractions with a small surface area (exposure to their 'outside world' - e.g. via function signatures) that are very deep (they hide a lot of complexity) make more complex behaviour much easier to manage.

When the surface area is high and the depth is low is when the overhead of the abstraction tends to exceed its use value.

Re: The Cost of Abstraction (2016)

#48
post #13

Earlier quoted context omitted.

> Which is not to say that we shouldn't wrap dependencies. We absolutely should. Why? If I'm writing something for a known environment that's unlikely to change, it seems like a waste of time.

Even then, often there will be small bits that don't fit well. By wrapping everything you make the model and assumptions of your application explicit. Code that is written against your own model (which is almost always simpler than what the library offers) will be more understandable. I really like the Dijkstra quote, "The purpose of abstraction is not to be vague, but to create a new semantic level in which one can…

I think this is nonsense and leads to enterprise-y type patterns and hard to understand code.

Wrapping things for the sake of it is a recipe for unnecessary verbosity and a hard to learn codebase as things are obfuscated and take longer to trace through.

In C, your point 1 makes me think your code is unsafe, if you're having trouble keeping track of types and signedness. Point 2 is fair enough, but I think a lot of C headers are granular enough that it's irrelevant, especially given how fast everything is these days. Gone are the 8 hour builds of old.

Point 3 is precisely the type of premature interface-isation that's the problem here. If you're never going to change it, then it doesn't matter whether huge portions of the code rely on the library or your wrapper. It's basically the same thing. Only you've put in extra work to wrap something in another layer for no gain. 4 much the same.

Re: The Cost of Abstraction (2016)

#49
post #13

Earlier quoted context omitted.

Which is not to say that we shouldn't wrap dependencies. We absolutely should. But by default, the wrapper should not offer an abstraction that is so flexible that the dependency can be exchanged. Instead, it should start with the actual problem, by offering only abstractions that (a) make sense in the context of the program and (b) translate well to concepts of the dependency/library. For example, hiding POSIX-style…

> Which is not to say that we shouldn't wrap dependencies. We absolutely should. Why? If I'm writing something for a known environment that's unlikely to change, it seems like a waste of time.

There's case for having a "convenience" or "easy mode" wrapper where the amount of configuration is lower, hence the underlying dependency is more straightforward to reuse.

For example, I would point to 3D graphics APIs as a reasonable case for this. All the ones in use today assume a lot of low-level detail about buffers and pointers and strides and attribute flags, which makes building up test cases challenging: you have to consider dozens to hundreds of options, and setting one wrongly will result in no image or a crash.

So, instead, many folks turn to copy-paste of known working examples, give them a minimal amount of additional configuration, and extend that into the frontend that they habitually use, rather than directly accessing the API in question. The full power is still there - it's not really abstracted - but the workflow has been pushed towards the average case.

Re: The Cost of Abstraction (2016)

#50
post #48

Earlier quoted context omitted.

Even then, often there will be small bits that don't fit well. By wrapping everything you make the model and assumptions of your application explicit. Code that is written against your own model (which is almost always simpler than what the library offers) will be more understandable. I really like the Dijkstra quote, "The purpose of abstraction is not to be vague, but to create a new semantic level in which one can…

I think this is nonsense and leads to enterprise-y type patterns and hard to understand code. Wrapping things for the sake of it is a recipe for unnecessary verbosity and a hard to learn codebase as things are obfuscated and take longer to trace through. In C, your point 1 makes me think your code is unsafe, if you're having trouble keeping track of types and signedness. Point 2 is fair enough, but I think a lot of C…

Hmm, I think I should put that in context. I'm not saying write a wrapper for the sake of it. I don't wrap everything in three layers of OO giftwrap. I'm saying one should follow basic rules of hygiene.

For example, I have a FreeType backend in my code, and I have a single file which implements a font interface (open font file, choose font size, draw to texture in the format of my app) by interfacing with Freetype. It returns my own error values, prints warnings with my own logging framework, puts the data in my own datastructures, etc. All things that Freetype could never do since it does not know my project.

I have an OpenGL backend, and I make sure that my geometry and math stuff, UI drawing code, and what not, does not depend on OpenGL, or OpenGL types. (Did that once when I learned OpenGL, and it was terrible). So now I have basically one file which is dependent on OpenGL, and it's there to simply put my internal datastructures on the screen, in a very straightforward way.

Same goes for my windowing backend. I use GLFW currently but I make sure I don't use e.g. GLFW_KEY_A in client code. I define my own enums and own abstractions. It does not come with any cost to simply have my own KEY_A, which means I can swap out the backend relatively easily, and can change my model more easily (for example, make up my own controls abstraction instead of relying on a standard PC 104 keys keyboard, etc).

> In C, your point 1 makes me think your code is unsafe, if you're having trouble keeping track of types and signedness.

No, it's safer precisely because I have a well-defined boundary where values get converted. I can check for overflows there, and not worry about the rest of my code. There is no practical way to deal with size_t in every little place when normal values come as int.

> Point 2 is fair enough, but I think a lot of C headers are granular enough that it's irrelevant,

You bet. Have you ever tried to access parts of the Win32 API? You're immediately in for (I think it was) 30K lines of code even if you define WIN32_LEAN_AND_MEAN. Otherwise it's maybe more like 60K. It's crazy. Or check out GLEW (which I don't use it anymore). It generates 27000 lines of header boilerplate simply to access the OpenGL API.

Add to that that most headers have a standard #ifdef include guard, which means that while files get ignored the second time, the lexer has to parse the whole thing over and over again.

> If you're never going to change it

How would you know?

> then it doesn't matter whether huge portions of the code rely on the library or your wrapper.

It does matter a lot if there is a semantic mismatch.

Post reply on HN