読者です 読者をやめる 読者になる 読者になる

ゲーム業界エンジニアの独り言

クライアント、サーバ、インフラなどなど興味あることなんでも紹介

ラッピングパターン(プロキシ、アダプター、ファサード)

今、「C++のためのAPIデザイン」を読んでます。 ラッピングで使用するデザインパターンについてまとめてみました。

C++のためのAPIデザイン

C++のためのAPIデザイン

ラッピングについて・・・

APIを設計する上で、クラスの集合の上に被せるようなラッパーインターフェイスを 記述することはよくあります。

例えば・・・

  • 大量のレガシーコードをベースに使う場合に、ベースのアーキテクチャを変更するのではなく、レガシーコードを隠蔽するようなクリーンなAPIを新たに設計する場合
  • C++APIを既に記述してあり、プレーンなCのAPIを開示する必要が出た場合

ラッパーのデメリットは、関節性のレイヤーが増えることで余分な状態を 保存することになりパフォーマンスに影響が出ることがあります。 とはいえ、先ほど述べた例に対して高品質で焦点を絞ったAPI設計が出来れば コストは十分に元は取れると思います。

ラッピングに使用するデザインパターン

あるインターフェイスを別のインターフェイスでラップする処理は、 構造に関するデザインパターンを使う事が出来ます。

上記は、ラッパーレイヤーと元のインターフェイス間の距離順に並んでいます。

プロキシパターン

プロキシパターンは、別のクラスへの1対1の転送インターフェイスを提供します。 つまり、プロキシクラスのFunctionA()を呼び出すと元のクラスのFunctionB()が 呼び出されます。 すなわち、プロキシクラスと元のクラスのインターフェイスは同じになります。

以下に例を示します。

class Proxy
{
public:
  // コンストラクタ
  Proxy() : original_(new Original())
  {
  }
  // デストラクタ
  ~Proxy()
  {
    delete original_;
  }
  bool DoSomething()
  {
    return original_->DoSomething();
  }

private:
  Original original_;
};

通常、プロキシパターンはプロキシクラスに元のクラスのポインタを 保存させて実装します。 プロキシクラスのメソッドは元のオブジェクトの同名メソッドにリダイレクトするだけです。

このテクニックのデメリットは、元のオブジェクトの関数を再提示、 コードの重複と基本的に同じプロセスする必要があることです。 そこで、この手法を使う場合は元のオブジェクトを変更する時には プロキシインターフェイスの整合性を維持するように継続的に努力する必要があります。

この手法を更に強化する方法として、プロキシAPIと元のAPIに共有させる 抽象インターフェイスを使う方法があります。 これは双方のAPIの同期をさせ続ける良い方法ですが、元のAPIが修正出来る必要があります。

// 抽象インターフェイス
class IOriginal
{
public:
  virtual bool DoSomething() = 0;
};

// 元のAPIクラス
class Original: public IOriginal
{
public:
  bool DoSomething();
};

// プロキシクラス
class Proxy : public IOriginal
{
public:
  // コンストラクタ
  Proxy() : original_(new Original())
  {
  }
  // デストラクタ
  ~Proxy()
  {
    delete original_;
  }
  bool DoSomething()
  {
    return original_->DoSomething();
  }

private:
  Original original_;
};

プロキシパターンのユースケース

  • 元のオブジェクトのレイジーインスタンス:このケースでは元のオブジェクトはコンストラクタでインスタンス化されますが、インスタンス化が高負荷の場合は引き伸ばしが可能です。
  • 元のオブジェクトへのアクセス制御:ProxyとOriginalの間に許可レイヤーを挿入し、Originalの特定メソッドしか呼び出せないようにします。
  • Originalクラスをスレッドセーフにミューテックスロックを追加します。最も効率の良い方法とは言えませんが、Originalを変更できない場合に有用です。
  • リソース共有のサポート:同じOriginalクラスを複数のProxyクラスで共有します。参照カウンタの実装や、データを共有しメモリ使用容量を最低限にします。
  • 将来的な変更に対してOriginalクラスを保護する:将来、Originalクラスに変更があった場合にプロキシクラスを通じてインターフェイスを保持することが出来ます。ただし、OriginalとProxyのインターフェイスに差異が出たため、プロキシパターンではなくなります。

アダプターパターン

アダプターパターンとは、あるクラスのインターフェイスを互換性のある 異なるインターフェイスに変換するものです。 プロキシと似ていますが、アダプターは元のクラスのインターフェイスとは異なります。

アダプターを使うと、ほかのコードで使うために既存のAPIの別のインターフェイスを 開示することが出来ます。

ここでは矩形の表示情報を定義するクラスを例に考えてみます。

class RectAdapter
{
public:
  // コンストラクタ
  RectAdapter() : rect_(new Rect())
  {
  }
  // デストラクタ
  ~RectAdapter()
  {
    delete rect_;
  }
  void Set(float x1, float y1, float x2, float y2)
  {
    // Rectクラスに合わせて中心座標とサイズに変更
    float w = x2 - x1;
    float h = y2 - y1;
    float cx = w / 2.f + x1;
    float cy = h / 2.f + y1;
    rect_->setDimensions(cx, cy, w, h);
  }

private:
  Rect rect_;
};

メソッドパラメータの順序が自分のAPIとは異なったり、座標システムが異なったり、 約束事が異なったり(中心とサイズ、左上、左下など)、メソッド命名法が 自分のAPIと異なったりする場合があります。 そこで、アダプタークラスを使ってインターフェイスAPIと互換性がある形に 変換します。

この例では、RectAdapterとRectでは異なるメソッド名や呼び出し規則を使って 矩形の設定をしています。 機能性はどちらも同じですが、別のインターフェイスを開示して簡単に処理できるようにします。

なお、アダプターは上の例を継承でも実装できます。 両者それぞれは、オブジェクトアダプターとクラスアダプターと呼ばれます。 パプリック継承も可能ですが、プライベート継承を使って新しいインターフェイスのみ 公開するほうがよいと思われます。

アダプターパターンの利点

  • APIの一貫性を強化:アダプターパターンを使うと、異なるインターフェイス様式を持つ異質のクラスをきちんとそろえ、全てに対して一貫したインターフェイスを提供できます。
  • APIの依存ライブラリをラップ:例えば、PNG画像をロードする機能を持つAPIの場合、libpngを使って実装したいがユーザーにはlibpngの存在を隠蔽し、将来的にlibpngの変更に備えて保護します。
  • データ型を変換:上記の例で示したように、異なるシステムや約束事を持つ2つのデータ型を内部で変換することが出来ます。
  • API用に異なる呼び出し規則を開示:例えば、プレーンなCのAPIを記述し、C++ユーザー向けにオブジェクト指向バージョンを提供したい場合に、Cの呼び出しをC++クラスにラップすることが出来ます。

ファサードパターン

ファサードは、他のクラスの集合に対してシンプルにしたインタフェースを提供します。 実際には、クラス群を使いやすくするためにそれらをまとめたハイレベルな インターフェイスを定義したいものです。 ファサードがクラスの構造を簡素化するのに対して、アダプターはクラスの構造を そのまま保持する点で異なっています。

では例を示します。

あなたは休暇でホテルに泊まっています。 夕食を食べてからショーを見に行くことにしました。 そのためには以下をする必要があります。 ・夕食のレストランの予約 ・劇場の席の予約 ・ホテルからのタクシーの予約 これら3つのやりとりをC++で表現してみます。

// タクシークラス
class Taxi
{
public:
  bool BookTaxi(int people, time_t time);
};
// レストランクラス
class Restaurant
{
public:
  bool ReserveTable(int people, time_t time);
};
// 劇場クラス
class Theater
{
public:
  time_t GetShowTime();
  bool ReserveSeats(int people);
};

さて、泊まっているホテルは高級で、優秀なコンシェルジュがこの全てを アレンジしてくれることにしましょう。 ショーの時間をつきとめ、適切な夕食の時間を割り出し、タクシーも手配してくれます。 これをC++設計で使う用語に変換すると、ずっとシンプルなインターフェイスを持つ 一つのオブジェクトとやりとりするだけで済むはずです。

// コンシェルジュクラス
class ConciergeFacade
{
public:
  time_t BookShow(int people);
};

ファサードパターンの応用

  • レガシーコードの隠蔽:壊れやすく一貫性のない朽ち果てたレガシーコードを相手にする場合は少なくありません。こうした場合、古いコード上に新たに分かりやすい設計のAPIをを作成したほうがずっと簡単になります。こうすれば新しいコードは全て新しいAPIを使うことになり、レガシーコードは完全に隠蔽出来ます。
  • コンビニエンスAPIの作成:一般的に柔軟性のあるルーチンにするか、共通ユースケースを簡単に処理できるシンプルで使いやすいルーチンにするか、決められない場合があります。ファサードなら両方のルーチンを共存させることが出来ます。(例として、OpenGLライブラリではローレベルの基本ルーチン(GLライブラリ)の提供と、基本ルーチン上に構築したハイレベルで使いやすいルーチン(GLUライブラリ)を提供している)
  • 機能のスワップAPIへのアクセスを抽象化することで、クライアントコードに影響を与えず特定の機能へ置き換えが可能です。これはデモやAPIのテストバージョンのサポートに使用できます。さらに、とあるゲーム用に別のレンダリングエンジンを使ったり、別の画像読み込みライブラリを使ったりするなど異なる機能へのスワップも可能になります。