I'm trying to write an abstraction layer to let my code run on different platforms. Let me give an example for two classes that I ultimately want to use in the high level code:
class Thread
{
public:
Thread();
virtual ~Thread();
void start();
void stop();
virtual void callback() = 0;
};
class Display
{
public:
static void drawText(const char* text);
};
My trouble is: What design pattern can I use to let low-level code fill in the implementation? Here are my thoughs and why I don't think they are a good solution:
In theory there's no problem in having the above definition sit in highLevel/thread.h and the platform specific implementation sit in lowLevel/platformA/thread.cpp. This is a low-overhead solution that is resolved at link-time. The only problem is that the low level implementation can't add any member variables or member functions to it. This makes certain things impossible to implement.
A way out would be to add this to the definition (basically the Pimpl-Idiom):
class Thread
{
// ...
private:
void* impl_data;
}
Now the low level code can have it's own struct or objects stored in the void pointer. The trouble here is that its ugly to read and painful to program.
I could make class Thread pure virtual and implement the low level functionality by inheriting from it. The high level code could access the low level implementation by calling a factory function like this:
// thread.h, below the pure virtual class definition
extern "C" void* makeNewThread();
// in lowlevel/platformA/thread.h
class ThreadImpl: public Thread
{ ... };
// in lowLevel/platformA/thread.cpp
extern "C" void* makeNewThread() { return new ThreadImpl(); }
This would be tidy enough but it fails for static classes. My abstraction layer will be used for hardware and IO things and I would really like to be able to have Display::drawText(...) instead of carrying around pointers to a single Display class.
Another option is to use only C-style functions that can be resolved at link time like this extern "C" handle_t createThread(). This is easy and great for accessing low level hardware that is there only once (like a display). But for anything that can be there multiple times (locks, threads, memory management) I have to carry around handles in my high level code which is ugly or have a high level wrapper class that hides the handles. Either way I have the overhead of having to associate the handles with the respective functionality on both the high level and the low level side.
My last thought is a hybrid structure. Pure C-style extern "C" functions for low level stuff that is there only once. Factory functions (see 3.) for stuff that can be there multiple times. But I fear that something hybrid will lead to inconsistent, unreadable code.
I'd be very grateful for hints to design patterns that fit my requirements.
You don't need to have a platform-agnostic base class, because your code is only compiled for a single concrete platform at a time.
Just set the include path to, for example, -Iinclude/generic -Iinclude/platform, and have a separate Thread class in each supported platform's include directory.
You can (and should) write platform-agnostic tests, compiled & executed by default, which confirm your different platform-specific implementations adhere to the same interface and semantics.
PS. As StoryTeller says, Thread is a bad example since there's already a portable std::thread. I'm assuming there's some other platform-specific detail you really do need to abstract.
PPS. You still need to figure out the correct split between generic (platform-agnostic) code and platform-specific code: there's no magic bullet for deciding what goes where, just a series of tradeoffs between reuse/duplication, simple versus highly-parameterized code, etc.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With