Is it common practice to abstract library dependencies from implementation?
Asked Answered
T

2

7

My answer to this question would be "no." But my coworkers disagree.

We're rebuilding our product and have a lot of critical decisions to make in the near-term.

While doing some of my own work I noticed that we've got some in-house C++ classes to abstract some of the POSIX API (threads, mutexes, semaphores, and rw locks) and other utility classes. Note that these classes are basic, and haven't been ported from Linux (portability is a factor in the rebuild.) We are also using POCO C++ libraries.

I brought this to the attention of my coworkers and suggested that we ditch our in-house classes in favour of their POCO equivalents. I want to take full advantage of the library we're already using. They suggested that we should implement our in-house classes using POCO, and further abstract additional POCO classes as necessary, so as not to depend on any specific C++ library (citing future unknowns - what if we want to use a different lib/framework like QT or boost, what if the one we choose turns out to be no good or development becomes inactive, etc.)

They also don't want to refactor legacy code, and by abstracting parts of POCO with our own classes, we can implement additional functionality (classic OOP.) Both of these arguments I can appreciate. However, I argue that if we're doing a recode we should go big, or go home. Now would be the time to refactor and it really shouldn't be that bad especially given the similarity between our classes and those in POCO (threads, etc.) I don't know what to say regarding the second point - should we only use extended classes where the functionality is necessary?

My coworkers also don't want to litter the POCO namespace all over the place. I argue that we should pick a library/framework/toolkit, and stick with it. Take full advantage of its features. Is this not typical practice? The only project I've seen that abstracts an entire framework is Freeswitch (that provides its own interface to APR.)

One suggestion is that the API we expose to each other, and potential customers, should be free of POCO, but it would be present in the implementation (which makes sense.)

None of us really have experience in these kinds of design decisions, and it shows in the current product. Having been at this since I was young, I've got some intuition that has brought me here, but no practical experience either. I really want to avoid poor solutions to problems that are already solved.

I think my question boils down to this: When building a product, should we a) choose a dominant framework on which to base most of our code, and b) expect that framework to be tightly coupled with the product? Isn't that the point of a framework? (Is framework or library more appropriate for POCO?)

Tympanum answered 11/9, 2012 at 15:7 Comment(0)
H
5

First, the API that you expose should definitely be free of POCO, boost, qt, or any other type that is not part of the standard C++ library. This is because the base libraries have their own release cycle, distinct from the release cycle of your library. If the users of your library also use boost, but a different, incompatible, version, they would need to spend time to resolve the incompatibility. The only exception to this rule is when you design a library to be released as part of a wider framework - say, an addition to the POCO toolkit. In this case the release of your library is tied to the release of the entire toolkit.

Internally, however, you should avoid using your own wrappers, unless the library that you are abstracting out is a true "commodity library"1. The reason for this is that when you hide an external library behind your classes, most of the time you mimic the level of abstraction of the library that you are hiding. The code that uses your wrapper will program to the level of abstraction dictated by the external library. When you swap the implementation behind your wrapper for a different framework, it is very likely that you would either (1) adapt the new framework to fit the level of abstraction of the old framework, or (2) will need to change the way in which you use your wrapper. Both cases are highly suspect: if you do (1), perhaps you shouldn't switch in the first place, and if you do (2), then your wrappers prove to be useless.


1 By "commodity library" I mean a library that provides a level of abstraction commonly found in other libraries that serve a similar purpose.
Hypertonic answered 11/9, 2012 at 15:33 Comment(4)
The second paragraph provided some valuable insight. I'd really like to hear more from experienced developers on the role third-party libraries and frameworks played in their product. For example, is the entire product based on the framework, and so tightly couple to that framework? What was the decision process in choosing a framework and so on.Tympanum
@Duke Much of this comes from my experience of working in a company that took writing wrappers to the max: they wrapped every single library, including the standard C library, for internal use. Wrapping of some libraries proved to be a win (database access, pthreads): we compiled the same code base on various UNIX flavors and on Windows, running against several DB vendors. We "null wrapped" pthreads on UNIX, and used our own wrapper on Windows. Wrappers of the standard C library proved to be a drag, however: they were "null-wrapped" on all platforms, essentially for nothing.Hypertonic
So you created a cross-platform thread wrapper that covered pthreads, etc? The situation I'm dealing with is wrapping the thread support in a third-party library that itself wraps pthreads and the windows equivalent (and any other thread implementation from the library-supported platforms.)Tympanum
@Duke It was another group in our company that wrapped pthreads; our group was using their wrapper. The company needed to build a wrapper in order to unify our windows and UNIX code without forcing everyone into the ifdef territory. We did not have access to a good third-party library that would abstract out the threading for us (this was back in 1998), so we built our own. You do have access to a good third-party library, so it may be possible for you to avoid the cost of building and testing your wrapper on multiple platforms.Hypertonic
U
5

There are two situations where I think it's worth having your own wrappers:

1) You've looked at several different mutex implementations on different systems/libraries, you've established a common set of requirements that they can all satisfy and that are sufficient for your software. Then you define that abstraction and implement it one or more times, knowing that you've planned ahead for flexibility. The rest of your code is written to rely only on your abstraction, not on any incidental properties of the current implementation(s). I have done this in the past, although not in code I can show you.

A classic example of this "least common interface" would be to change rename in the filesystem abstraction, on the basis that Windows cannot implement an atomic rename-over-an-existing-file. So your code must not rely on atomic rename-replacement if you might in future swap out your current *nix implementation for one that can't do that. You have to restrict the interface from the start.

When done right, this kind of interface can considerably ease any kind of future porting, either to a new system or because you want to change your third-party library dependencies. However, an entire framework is probably too big to successfully do this with -- essentially you'd be inventing and writing your own framework, which is not a trivial task and conceivably is a larger task than writing your actual software.

2) You want to be able to mock/stub/sham/spoof/plagiarize/whatever the next clever technique is, the mutex in tests, and decide that you will find this easier if you have your own wrapper thrown over it than if you're trying to mess with symbols from third-party libraries, or that are built-in.

Note that defining your own functions called wrap_pthread_mutex_init, wrap_pthread_mutex_lock etc, that precisely mimic pthread_* functions, and take exactly the same parameters, might satisfy (2) but doesn't satisfy (1). And anyway, doing (2) properly probably requires more than just wrappers, you usually also want to inject the dependencies into your code.

Doing extra work under the heading of flexibility, without actually providing for flexibility, is pretty much a waste of time. It can be very difficult or even provably impossible to implement one threading environment in terms of another one. If you decide in future to switch from pthreads to std::thread in C++, then having used an abstraction that looks exactly like the pthreads API under different names is (approximately) no help whatsoever.

For another possible change you might make, implementing the full pthreads API on Windows is sort of possible, but probably more difficult than only implementing what you actually need. So if you port to Windows, all your abstraction saves you is the time to search and replace all calls in the rest of your software. You're still going to have to (a) plug in a complete Posix implementation for Windows, or (b) do the work to figure out what you actually need, and only implement that. Your wrapper won't help with the real work.

Unruh answered 11/9, 2012 at 16:44 Comment(2)
They haven't written function wrappers, they've created classes that make use of those POSIX API functions - a typical "Runnable" with Start(), Stop(), Run(), internally handling calls to the pthreads API. So I guess an API wrapper rather than function wrapper. My contention is that this is all extra work to create untested (and probably untestable) components, where we could just use a lib/framework and get it all for free.Tympanum
Note that Threads is just one example. There are also components for Events, Crypto, IPC, Memory, Queues, Strings, and a few others. It's crazy to do this extra work, imho.Tympanum

© 2022 - 2024 — McMap. All rights reserved.