设计模式:C++抽象层

3

我正在尝试编写一个抽象层,让我的代码可以在不同的平台上运行。让我举个例子,这里有两个类,我最终希望在高级别代码中使用:

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

    void start();
    void stop();

    virtual void callback() = 0;
};

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

我的问题是:我可以使用什么设计模式让低级别的代码填写实现?以下是我的想法以及为什么我认为它们不是一个好的解决方案:
  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

我希望能得到一些适合我的需求的设计模式提示。


4
还没看完你的问题。但是自 C++11 起,C++ 提供了一个用于线程的抽象层。 - StoryTeller - Unslander Monica
我正在使用FreeRTOS作为嵌入式目标的线程处理工具。另一个平台是基于计算机的仿真,以便更轻松地开发高级别代码。我认为C++11不支持这种情况。 - LoveDaOOP
3个回答

1
你不需要一个平台无关的基类,因为你的代码一次只编译一个具体的平台。只需将包含路径设置为例如-Iinclude/generic -Iinclude/platform,并在每个支持的平台的包含目录中有一个单独的线程类。您可以(也应该)编写平台无关的测试,通过默认编译和执行,以确认您不同的平台特定实现遵循相同的接口和语义。PS.正如StoryTeller所说,Thread是一个糟糕的例子,因为已经有一个可移植的std::thread。我假设还有其他平台特定的细节需要抽象化。PPS.您仍然需要确定通用代码和平台特定代码之间的正确分割:没有决定哪里放置什么的魔法子弹,只有在重用/复制、简单与高度参数化的代码之间进行权衡的一系列交易。

1
当然不是一个单独的Thread类,而是Thread类的单独实现。我对包含路径不太明白,因为这些是源目录 :) - StoryTeller - Unslander Monica
如果我在不同的目录中编写两个不同的类(都称为Thread),从每个类中公开相同的公共接口但以不同的方式实现它们,并确保在给定的翻译单元和整个程序中只能包含一个... 它们是不同的类吗?是相同类的不同实现吗?这是一个没有有用区别的区分。 - Useless
ODR 表示系统中只有一个这样的类。它在高级头文件中定义。实现可能会有所不同。在语义上准确可以帮助 OP 弄清楚他们需要做什么。 - StoryTeller - Unslander Monica
设置您的包含路径(以及库路径,如果它不是仅头文件库)可以满足ODR。您要么正在构建(例如)PlatformX二进制文件,其中包括平台无关代码和PlatformX特定代码,要么正在构建PlatformY二进制文件,其中包括平台无关代码和PlatformY特定代码。您有任何平台特定内容的一个定义 - 但这取决于构建系统。 - Useless
我的问题是:如果我让特定平台的实现提供thread.hthread.cpp,那么本质上就是特定平台的代码定义了接口。我不喜欢这样。接口应该由高级代码定义。忽略“自然”层次结构似乎很粗糙和混乱。如果我让thread.h保留在高级代码中,并在每个平台的代码中有一个匹配的thread.cpp,那么我的特定平台实现就不能添加成员变量或继承等内容。 - 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目录中的所有Thread_*.cpp文件取出,并让您的构建系统为编译器提供正确的-D选项即可。

是的,我以前见过Pimpl惯用语。它基本上就是我最初帖子中的第2点。我认真考虑过这样做,但我发现代码变得非常丑陋。 - LoveDaOOP
@LoveDaOOP,如果你使用void*,那当然可以。但是如果你不绕过类型系统,那就不太行了。 - StoryTeller - Unslander Monica
我现在接受这个答案,因为它似乎是最简洁的解决方案。在我的项目中,我最终选择了一个简单的C风格接口(见我最初的帖子中的第4点)。在高层面上,我有包装类来隐藏所有的句柄。通过将句柄定义为typedef void* threadHandle_t,我可以让我的低级代码使用句柄作为适当的内容。通常情况下,句柄会被滥用为指向低级代码中对象的指针。这样,我就不必使用表格或映射来将句柄与其相应的对象关联起来。感谢大家! - LoveDaOOP

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对象时,您还会自动获得一个平台相关的类,该类自动设置其内部,并且可能具有平台特定操作,而且您的PlatformAThread类可能派生自具有您可能需要的共同事物的通用Base类。

您还需要设置构建系统以自动识别平台特定目录。

此外,请注意,我有创建类继承层次结构的倾向,而有些人建议不要这样做:https://en.wikipedia.org/wiki/Composition_over_inheritance


这是一个不错的想法。但它假设高级代码知道它将在哪个平台上运行。我可以使用带有字符串化的宏通过makefile提供正确的类名。像这样:`class Thread: public TREAD_IMPL`然后在构建设置中,我可以添加正确的预处理器定义。但这也有点混乱。 - LoveDaOOP

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接