Я пытаюсь написать слой абстракции, чтобы мой код работал на разных платформах. Позвольте мне привести пример для двух классов, которые я в конечном итоге хочу использовать в коде высокого уровня:
class Thread
{
public:
Thread();
virtual ~Thread();
void start();
void stop();
virtual void callback() = 0;
};
class Display
{
public:
static void drawText(const char* text);
};
Моя проблема в том, какой шаблон проектирования я могу использовать, чтобы низкоуровневый код заполнил реализацию?
Вот мои мысли и почему я не думаю, что они являются хорошим решением:
В теории нет проблем с тем, что приведенное выше определение находится в highLevel/thread.h
и реализация конкретной платформы сидеть в lowLevel/platformA/thread.cpp
, Это решение с минимальными издержками, которое разрешается во время соединения. Единственная проблема заключается в том, что низкоуровневая реализация не может добавить к ней никакие переменные-члены или функции-члены. Это делает некоторые вещи невозможными для реализации.
Выходом было бы добавить это к определению (в основном Pimpl-Idiom):
class Thread
{
// ...
private:
void* impl_data;
}
Теперь низкоуровневый код может иметь собственную структуру или объекты, хранящиеся в указателе void. Проблема здесь в том, что читать некрасиво и больно программировать.
Я мог бы сделать class Thread
чисто виртуальный и реализующий низкоуровневую функциональность путем наследования от него. Высокоуровневый код может получить доступ к низкоуровневой реализации, вызвав фабричную функцию следующим образом:
// 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(); }
Это было бы достаточно аккуратно, но не для статических классов. Мой уровень абстракции будет использоваться для аппаратных средств и операций ввода-вывода, и я бы очень хотел иметь возможность Display::drawText(...)
вместо того, чтобы носить с собой указатели на один Display
учебный класс.
Другой вариант — использовать только функции в стиле C, которые могут быть разрешены во время соединения, как это extern "C" handle_t createThread()
, Это просто и отлично подходит для доступа к низкоуровневому оборудованию, которое есть только один раз (например, к дисплею). Но для всего, что может быть там несколько раз (блокировки, потоки, управление памятью), я должен носить с собой дескрипторы в своем коде высокого уровня, который уродлив или имеет класс-оболочку высокого уровня, который скрывает маркеры. В любом случае, мне приходится связывать маркеры с соответствующими функциями как на высоком, так и на низком уровне.
Моя последняя мысль — гибридная структура. Чистый C-стиль extern "C"
функции для вещей низкого уровня, которые есть только один раз. Фабричные функции (см. 3.) для вещей, которые могут быть там несколько раз. Но я боюсь, что что-то гибридное приведет к непоследовательному, нечитаемому коду.
Я был бы очень благодарен за подсказки для разработки шаблонов, которые соответствуют моим требованиям.
Вы, кажется, хотите семантику значения для вашего Thread
Класс и интересно, куда добавить косвенность, чтобы сделать его переносимым. Таким образом, вы используете идиому pimpl и некоторую условную компиляцию.
В зависимости от того, где вы хотите, чтобы сложность вашего инструмента сборки была, и если вы хотите сохранить весь низкоуровневый код как можно более автономным, вы делаете следующее:
В вас заголовок высокого уровня Thread.hpp
Вы определяете:
class Thread
{
class Impl:
Impl *pimpl; // or better yet, some smart pointer
public:
Thread ();
~Thread();
// Other stuff;
};
Затем в каталоге исходных текстов вы определяете файлы следующим образом:
Thread_PlatformA.cpp
#ifdef PLATFORM_A
#include <Thread.hpp>
Thread::Thread()
{
// Platform A specific code goes here, initialize the pimpl;
}
Thread::~Thread()
{
// Platform A specific code goes here, release the pimpl;
}
#endif
Строительство Thread.o
становится простым вопросом принятия всех Thread_*.cpp
файлы в каталоге Thread, и ваша система сборки предложит правильные -D
вариант для компилятора.
Вам не нужно иметь независимый от платформы базовый класс, потому что ваш код компилируется только для одной конкретной платформы за раз.
Просто установите путь для включения, например, -Iinclude/generic -Iinclude/platform
и иметь отдельный класс Thread в каждом поддерживаемом ПлатформаВключите каталог.
Вы можете (и должны) писать независимые от платформы тесты, скомпилированные & выполняется по умолчанию, что подтверждает, что ваши различные реализации для конкретной платформы придерживаются одного и того же интерфейса и семантики.
PS. Как говорит StoryTeller, Thread — плохой пример, поскольку уже есть портативный std::thread
, Я предполагаю, что есть некоторые другие специфичные для платформы детали, которые вам действительно нужно абстрагировать.
PPS. Вам все еще нужно выяснить правильное разделение между общий (независимый от платформы) код и специфичный для платформы код: нет волшебной палочки для решения, что и куда идти, просто ряд компромиссов между повторным использованием / дублированием, простым и высокопараметрическим кодом и т. д.
Мне любопытно, каково было бы проектировать эту ситуацию следующим образом (просто придерживаясь темы):
// Your generic include level:
// thread.h
class Thread : public
#ifdef PLATFORM_A
PlatformAThread
#elif PLATFORM_B
PlatformBThread
// any more stuff you need in here
#endif
{
Thread();
virtual ~Thread();
void start();
void stop();
virtual void callback() = 0;
} ;
который не содержит ничего о реализации, только интерфейс
Тогда у вас есть:
// platformA directory
class PlatformAThread { ... };
и это автоматически приведет к тому, что при создании вашего «универсального» Thread
объект, который вы автоматически получаете также платформо-зависимый класс, который автоматически устанавливает свои внутренние функции и который может иметь специфичные для платформы операции, и, конечно, ваш PlatformAThread
класс может происходить из общего Base
класс, имеющий общие вещи, которые вам могут понадобиться.
Вам также необходимо настроить систему сборки на автоматическое распознавание каталогов, специфичных для платформы.
Также обратите внимание, что у меня есть склонность создавать иерархии наследования классов, и некоторые люди советуют это: https://en.wikipedia.org/wiki/Composition_over_inheritance