【问题标题】:Design pattern: C++ Abstraction Layer设计模式:C++ 抽象层
【发布时间】:2016-08-01 12:31:36
【问题描述】:

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

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-Idiom):

    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.)。但我担心混合的东西会导致不一致、不可读的代码。

如果您能提供符合我要求的设计模式提示,我将不胜感激。

【问题讨论】:

  • 尚未阅读您的整个问题。但是 C++ 从 c++11 开始就有一个线程抽象层。
  • 我正在使用 FreeRTOS 开发一个嵌入式目标,以提供线程的东西。另一个平台是基于计算机的仿真,用于更轻松地开发高级代码。我认为 C++11 不支持这种情况。

标签: c++ c abstraction hal


【解决方案1】:

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

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

您可以(并且应该)编写与平台无关的测试,默认编译和执行,以确认您的不同平台特定实现遵循相同的接口和语义。

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

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

【讨论】:

  • 肯定不是单独的Thread 类,而是Thread 类的单独实现。而且我不遵循包含路径,因为这些是源目录:)
  • 如果我在不同的目录中编写两个不同的类(都称为Thread),从每个暴露相同的公共接口但不同地实现它们,并确保只有一个可以包含在给定的翻译单元和整个程序中……它们是不同的类吗?同一个类的不同实现?这是一个没有有用区别的区别。
  • ODR 说系统中只有一个这样的类。它在高级标题中定义。实施可能会有所不同。准确的语义可以帮助 OP 弄清楚他们需要做什么。
  • 设置包含路径(和 lib 路径,如果它不是仅标头库)满足 ODR。您要么从平台无关代码加上 PlatformX 特定代码构建(比如说)PlatformX 二进制文件,要么从与平台无关代码加上 PlatformY 特定代码构建 PlatformY 二进制文件。您对任何特定于平台的东西都有一个定义 - 但由构建系统决定
  • 我的问题是:如果我让平台特定的实现同时提供thread.hthread.cpp,那么基本上平台特定的代码定义了接口。我不喜欢那样。接口确实应该由高级代码定义。忽略“自然”层次结构似乎很笨拙和混乱。如果我让thread.h 位于高级代码中并在每个平台代码中都有一个匹配的thread.cpp,那么我的平台特定实现不能添加成员变量或继承等。
【解决方案2】:

您似乎想要 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 选项。

【讨论】:

  • 是的,我以前见过 Pimpl 成语。它基本上是我最初帖子的 2.我认真考虑过这样做,但我发现代码变得非常丑陋。
  • @LoveDaOOP,如果你使用void* 当然可以。如果你不规避类型系统,那就不是那么多了。
  • 我现在接受这个作为答案,因为它似乎是最整洁的解决方案。在我的项目中,我最终选择了一个简单的 C 风格界面(4. 在我最初的帖子中)。在高级方面,我有隐藏所有句柄的包装类。通过将句柄定义为typedef void* threadHandle_t,我可以让我的低级代码使用任何看起来合适的句柄。通常,句柄会被滥用为指向低级代码中对象的指针。这样我就不必使用表格或地图将句柄与它们各自的低级对象相关联。谢谢大家!
【解决方案3】:

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

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

【讨论】:

  • 这是个好主意。但它假设高级代码知道它将在什么平台上运行。我可以使用带有 stringyfication 的宏来通过 makefile 提供正确的类名。像这样:class Thread: public TREAD_IMPL 然后在构建设置中我可以添加正确的预处理器定义。但这也有点乱。
猜你喜欢
  • 1970-01-01
  • 2010-09-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多