2016-08-01 2 views
3

Я пытаюсь написать слой абстракции, чтобы мой код работал на разных платформах. Позвольте мне привести пример для двух классов, которые я в конечном счете, хотят использовать в коде высокого уровня:Дизайн шаблона: C++ Уровень абстракции

class Thread 
{ 
public: 
    Thread(); 
    virtual ~Thread(); 

    void start(); 
    void stop(); 

    virtual void callback() = 0; 
}; 

class Display 
{ 
public: 
    static void drawText(const char* text); 
}; 

Моя беда в том: Какой дизайн шаблон можно использовать, чтобы досыта код низкого уровня в реализации? Вот мое thoughs и почему я не думаю, что они являются хорошим решением:

  1. В теории нет никаких проблем в том, вышеуказанных сидячих определениях в highLevel/thread.h и конкретную реализации платформы сидеть в lowLevel/platformA/thread.cpp. Это низкозатратное решение, которое разрешено во время соединения. Единственная проблема заключается в том, что реализация на низком уровне не может добавлять к ней какие-либо переменные-члены или функции-члены. Это делает невозможным реализацию определенных вещей.

  2. Выходом было бы добавить это определение (в основном Pimpl-Idiom):

    class Thread 
    { 
        // ... 
    private: 
        void* impl_data; 
    } 
    

    Теперь код низкого уровня может иметь свои собственные структуры или объекты, хранящиеся в указателе ничтожной. Беда здесь в том, что ее уродливые, чтобы читать и мучить программу.

  3. Я мог бы сделать 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.

  4. Другой вариант - использовать только функции стиля С, которые могут быть разрешены во время соединения, например, extern "C" handle_t createThread(). Это легко и удобно для доступа к низкоуровневому оборудованию, которое есть только один раз (например, дисплей). Но для чего-то, что может быть несколько раз (блокировки, потоки, управление памятью), я должен носить с собой ручки в моем коде высокого уровня, который является уродливым или имеет класс обертки высокого уровня, который скрывает ручки. В любом случае у меня есть накладные расходы, связанные с необходимостью связывать ручки с соответствующими функциями как на высоком уровне, так и на стороне низкого уровня.

  5. Моя последняя мысль - гибридная структура. Pure C-style extern "C" функции для низкоуровневого материала, который есть только один раз. Заводские функции (см. 3.) для вещей, которые могут быть там несколько раз. Но я боюсь, что что-то гибридное приведет к непоследовательному, нечитаемому коду.

Я был бы очень признателен за советы по дизайну образцов, соответствующих моим требованиям.

+4

Не читал весь ваш вопрос еще. Но C++ имеет уровень абстракции для потоков с C++ 11. – StoryTeller

+0

Я работаю над встроенной целью с FreeRTOS, поддерживая материал для потоковой передачи. Другая платформа - это эмуляция на базе компьютера для упрощения разработки кода высокого уровня. Я не думаю, что C++ 11 поддерживает этот сценарий. – LoveDaOOP

ответ

0

Кажется, вам нужна семантика значений для вашего класса 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 для компилятора.

+0

Да, я видел идиома Pimpl раньше. Это в основном 2. моего начального поста. Я серьезно подумал об этом, но считаю, что код становится действительно уродливым. – LoveDaOOP

+0

@LoveDaOOP, если вы используете 'void *' sure. Если вы не обходите систему типов, чем не так много. – StoryTeller

+0

Я принимаю это как ответ сейчас, потому что это, кажется, самое чистое решение. В моем проекте я в конечном итоге пошел на простой интерфейс стиля C (4. в моем первоначальном посте). На стороне высокого уровня у меня есть классы-оболочки, которые скрывают все дескрипторы. Определив дескриптор как 'typedef void * threadHandle_t', я могу использовать свой код нижнего уровня, используя дескриптор, как кажется подходящим. Часто ручка будет злоупотреблять указателем на объект в коде низкого уровня. Таким образом, мне не нужно использовать таблицы или карты для связывания ручек с их соответствующими объектами на стороне низкого уровня. Спасибо вам всем! – LoveDaOOP

1

Вам не нужно иметь базовый класс платформы-агностика, потому что ваш код только скомпилирован для отдельной бетонной платформы за раз.

Просто установите путь включения, например, -Iinclude/generic -Iinclude/platform, и укажите отдельный класс Thread на каждой поддерживаемой платформе .

Вы можете (и должны) писать тесты на платформу-агностик, скомпилированные по умолчанию &, которые подтверждают, что ваши разные реализации на платформе придерживаются одного и того же интерфейса и семантики.

PS. Как говорит StoryTeller, Thread - плохой пример, поскольку уже есть портативный std::thread. Я предполагаю, что есть некоторые другие специфичные для платформы детали, которые вам действительно нужно абстрагироваться.

PPS. Вам все равно нужно выяснить правильный раскол между generic (платформо-агностик) код и код для конкретной платформы: нет никакой волшебной пули для решения, что происходит, только серия компромиссов между повторным использованием/дублированием, простой и высокопараметризированный код и т. д.

+1

Наверняка не отдельный класс 'Thread', а отдельные * реализации * класса' Thread'. И я не следую с пути include, так как это исходные каталоги :) – StoryTeller

+0

Если я пишу два разных класса (оба называются 'Thread') в разных каталогах, выставляйте один и тот же публичный интерфейс из каждого, но реализуйте их по-разному, и _only one_ может быть включен в данную единицу перевода и всю программу ... являются ли они разными классами? Различные реализации одного класса? Это различие без полезной разницы. – Useless

+0

ODR говорит, что в системе есть только один такой класс. И это определено в заголовке высокого уровня. Реализация может отличаться. И точное определение семантики может помочь ОП выяснить, что им нужно делать. – StoryTeller

0

Мне интересно, что бы это было как разработать такую ​​ситуацию, как следующее (просто прилипает к теме):

// 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 объекта автоматически г et также класс, зависящий от платформы, который автоматически настраивает свои внутренние компоненты и может иметь специфичные для платформы операции, и, конечно, ваш класс PlatformAThread может быть получен из общего класса Base, имеющего общие вещи, которые могут вам понадобиться.

Вам также необходимо настроить систему сборки, чтобы автоматически распознавать каталоги, специфичные для платформы.

Кроме того, обратите внимание, что у меня есть тенденция к созданию иерархии класса наследств, и некоторые люди советуют против этого: https://en.wikipedia.org/wiki/Composition_over_inheritance

+0

Это хорошая идея. Но предполагается, что код высокого уровня знает, на какой платформе он будет работать. Я мог бы использовать макросы с stringyfication для предоставления правильного имени класса через make-файлы. Пример: 'class Тема: public TREAD_IMPL' Тогда в настройках сборки я мог бы добавить правильные определения препроцессора. Но это тоже беспорядочно. – LoveDaOOP

Смежные вопросы