设计模式:C++抽象层

Design pattern: C++ Abstraction Layer

本文关键字:抽象 C++ 设计模式      更新时间:2023-10-16

我正试图编写一个抽象层,让我的代码在不同的平台上运行。让我举一个例子,说明我最终想要在高级代码中使用的两个类:

class Thread
{
public:
    Thread();
    virtual ~Thread();
    void start();
    void stop();
    virtual void callback() = 0;
};
class Display 
{
public:
    static void drawText(const char* text);
};

我的问题是:我可以使用什么设计模式来让低级代码填充实现?以下是我的想法,以及为什么我认为它们不是一个好的解决方案:

  1. 理论上,将上述定义放在highLevel/thread.h中并且将特定于平台的实现放在lowLevel/platformA/thread.cpp中是没有问题的。这是一个低开销的解决方案,可在链路时解决。唯一的问题是,低级别实现无法向其中添加任何成员变量或成员函数。这使得某些事情无法实现。

  2. 一个解决办法是将其添加到定义中(基本上是Pimpl习语):

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

    现在,低级代码可以在void指针中存储自己的结构或对象。这里的问题是它读起来很难看,编程起来很痛苦。

  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(); }
    

    这已经足够整洁了,但对于静态类来说却失败了。我的抽象层将用于硬件和IO,我真的希望能够使用Display::drawText(...),而不是携带指向单个Display类的指针。

  4. 另一种选择是只使用可以在链接时解析的C样式函数,如extern "C" handle_t createThread()。这对于访问只存在一次的低级别硬件(如显示器)来说既简单又方便。但对于任何可能多次出现的东西(锁、线程、内存管理),我必须在我的高级代码中携带句柄,这很难看,或者有一个隐藏句柄的高级包装器类。无论哪种方式,我都需要将句柄与高层和底层的相应功能相关联。

  5. 我最后一个想法是混合结构。纯C风格的extern "C"函数用于只存在一次的低级内容。工厂函数(请参见3.)中可以存在多次的东西。但我担心一些混合的东西会导致不一致、不可读的代码。

如果能给我一些提示,让我设计出符合我要求的图案,我将不胜感激。

您不需要有一个与平台无关的基类,因为您的代码一次只为单个具体平台编译。

例如,只需将include路径设置为-Iinclude/generic -Iinclude/platform,并在每个受支持的平台的include目录中有一个单独的Thread类。

您可以(也应该)编写与平台无关的测试,编译&默认情况下执行,以确认不同的特定于平台的实现遵守相同的接口和语义。

PS。正如StoryTeller所说,Thread是一个糟糕的例子,因为已经有了一个可移植的std::thread。我假设你确实需要抽象一些其他特定于平台的细节。

PPS。您仍然需要弄清楚通用(与平台无关)代码和特定于平台的代码之间的正确划分:没有什么灵丹妙药可以决定去哪里,只有重用/重复、简单代码与高度参数化代码之间的一系列权衡。

您似乎想要Thread类的值语义,并想知道在哪里添加间接性以使其可移植。所以你使用了皮条习语和一些条件汇编
根据您希望构建工具的复杂性所在的位置,如果您希望尽可能保持所有低级别代码的自包含性,请执行以下操作:

在您的高级标头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选项。

我很好奇,像下面这样设计这种情况会是什么样子(只是坚持线程):

// 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