5 ms·
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 t
by jstimpfle 8y ago
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 select/poll vs Win32 Completion ports behind a common abstraction is something I tried recently, and seems to be HARD. Did not complete. Why? I started with the abstraction without being familiar with what the problem was. All that I knew was that I wanted to experiment with "game engine design".
- Nursie 8y ago> 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.
- jstimpfle 8y agoEven 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 be absolutely precise." Usually I even wrap libc and POSIX functionality in my C programs. Advantages include (1) not having to mess with specific types, such as size_t, time_t, DIR*, and so on -- which often don't fit my own model (e.g. dealing with unsigned size_t vs signed integers is painful), and often need small fixes to be portable. (2) compiles a lot faster since I don't need to include 10000s of lines from system header files (3) not wrapping makes huge portions of the project dependent on the library, instead of only the wrapper. (4) You tightly control access to the library, which makes it easier to troubleshoot. You can also make up your own strategy for managing library objects in a central place.
- pishpash 8y agoYes, a library is already an abstraction, just somebody else's. Thread parent is complaining about bad design that doesn't model the problem, not abstraction.
- jstimpfle 8y agoWhat do you mean? Bad design in the library? In a wrapper? What's the difference between "design" and "abstraction" here?
- Nursie 8y agoI 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.
- jstimpfle 8y agoHmm, 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.
- mntmoss 8y agoThere'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.
- Nursie 8y agoI wouldn't necessarily consider that a wrapper, personally, but a reasonable set of functionality behind an interface to said functionality. The only thing I'm really arguing with is the insistence that one should wrap everything as a matter of course. Reduction of complexity, like your example, is great. Increasing levels of indirection thoughtlessly is adding to it, IMHO.
- jstimpfle 8y agoBut that's exactly what I was describing all the time...
- Nursie 8y agoI got the impression you were saying every dependency should always be wrapped, regardless. I was a C coder for many years, these days I seem to be doing java. There are dependencies in my recent projects that just do what I need, no particular domain translation required. Particularly things like the Apache commons libraries, which provide well formed utilities for common operations. It would be a waste of time and energy to wrap them simply for the sake of having a wrapper. If this sort of thing isn't what you were driving at, then we've just been miscommunicating. I am 100% for encapsulation of functionality into good, discrete modules which provide sensible interfaces and minimal (but expressive APIs). I just don't like the blind application of "this isn't our code therefore we must provide an interface"
- z3t4 8y agoDependencies should be treated like if it was your own code. After you have replaced some API for the third time you can start thinking about wrapping it.
- drawkbox 8y agoOr 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 Amazon Game Circle, web or custom. These libraries are frequently updated, they change per platform and you will touch them many places in the codebase, so wrapping them in a progression/statistics/achievements wrapper/facade/adapter is smart from the start.
- z3t4 8y agoEven 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.)
- drawkbox 8y agoIf 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.