3

コードをさまざまなプラットフォームで実行できるように、抽象化レイヤーを作成しようとしています。高レベル コードで最終的に使用したい 2 つのクラスの例を挙げましょう。

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

    これで十分ですが、静的クラスでは失敗します。Display::drawText(...)私の抽象化レイヤーは、ハードウェアと IO に使用されます。単一のDisplayクラスへのポインターを持ち運ぶ代わりに、それを使用できるようにしたいと考えています。

  4. 別のオプションは、このようにリンク時に解決できる C スタイルの関数のみを使用することextern "C" handle_t createThread()です。これは簡単で、一度しか存在しない低レベルのハードウェア (ディスプレイなど) にアクセスするのに最適です。しかし、複数回存在する可能性のあるもの (ロック、スレッド、メモリ管理) については、ハンドルを隠すか、ハンドルを非表示にする高レベルのラッパー クラスを持つ高レベル コードでハンドルを持ち歩く必要があります。いずれにせよ、高レベル側と低レベル側の両方でハンドルをそれぞれの機能に関連付ける必要があるというオーバーヘッドがあります。

  5. 私の最後の考えは、ハイブリッド構造です。extern "C"一度しか存在しない低レベルのもののための純粋な C スタイルの関数。複数回存在する可能性のあるもののファクトリ関数 (3. を参照)。しかし、私はハイブリッドなものが一貫性のない、読めないコードにつながるのではないかと心配しています。

私の要件に合ったパターンを設計するためのヒントをいただければ幸いです。

4

3 に答える 3

1

コードは一度に 1 つの具体的なプラットフォーム用にのみコンパイルされるため、プラットフォームに依存しない基本クラスを用意する必要はありません。

たとえば、インクルード パスを に設定し、サポートされている各プラットフォームのインクルード ディレクトリ-Iinclude/generic -Iinclude/platformに別の Thread クラスを配置します。

デフォルトでコンパイルおよび実行される、プラットフォームに依存しないテストを作成できます (作成する必要があります)。これにより、さまざまなプラットフォーム固有の実装が同じインターフェイスとセマンティクスに準拠していることを確認できます。

PS。StoryTeller が言うように、 Thread は悪い例std::threadです。本当に抽象化する必要があるプラットフォーム固有の詳細が他にもあると思います。

PPS。一般的な(プラットフォームにとらわれない) コードとプラットフォーム固有のコードとの間の正しい分割を把握する必要があります。何がどこにあるのかを決定するための魔法の弾丸はなく、再利用/複製、単純なコードと高度にパラメーター化されたコードの間の一連のトレードオフだけです。等

于 2016-08-01T12:41:28.350 に答える
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

于 2016-08-01T13:06:56.760 に答える
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 ディレクトリ内のすべてのファイルを取得し、ビルド システムにコンパイラへの正しいオプションを与えるThread.oという単純な問題になります。Thread_*.cpp-D

于 2016-08-01T13:04:00.607 に答える