ゲームプログラマのための設計シリーズ:デザインパターン編の記事です。
概要
- 事前処理・事後処理を確実に呼び出したいなら、RAII・ローンパターン・Template Methodパターンが考えられる
- スコープ外に持ち出す必要があるならRAII
- そうでなければローンパターンがオススメ
本文
こんなAPIがあるとします。
class Graphics
{
public:
void beginDraw(); // 描画開始
void drawXXXX(); // 具体的な描画
void drawYYYY(); // 具体的な描画
void endDraw(); // 描画終了
};
beginDraw/endDrawが「具体的な描画」関数の呼び出し前後に確実に呼ばれるようにするにはどうしたらよいでしょうか。
RAII
まずは、おなじみのRAIIです。
class RAIIDrawer final
{
public:
explicit RAIIDrawer(Graphics& g) : mGraphics(g) { mGraphics.beginDraw(); }
~RAIIDrawer() { mGraphics.endDraw(); }
// ※コピーやムーブの実装は要注意。必要なければ消しておくのがオススメ
RAIIDrawer(const RAIIDrawer&) = delete;
RAIIDrawer(RAIIDrawer&&) = delete;
// begin/endの間に行ってほしい操作へのプロキシ
void drawXXXX() { mGraphics.drawXXXX(); }
void drawYYYY() { mGraphics.drawYYYY(); }
private:
Graphics& mGraphics;
};
void test(Graphics& graphics)
{
RAIIDrawer drawer(graphics); // DrawerのコンストラクタでbeginDraw
drawer.drawXXXX();
drawer.drawYYYY();
// DrawerのデストラクタでendDraw
}
この方法はRAIIオブジェクトをスコープの外に持ち出せるメリットがあります。
が、それゆえに
- RAIIオブジェクトのコピー/ムーブの実装には気を使わないといけない
- RAIIオブジェクトより先にGraphicsの寿命が尽きてしまい不正な参照となる
という危険も存在します。
ローンパターン
もう一つの解は、高階関数を使う方法です。
// beginDraw/endDrawを隠す
class DrawProxy
{
public:
explicit DrawProxy(Graphics& g) : mGraphics(g) {}
// begin/endの間に行ってほしい操作へのプロキシ
void drawXXXX() { mGraphics.drawXXXX(); }
void drawYYYY() { mGraphics.drawYYYY(); }
private:
Graphics mGraphics;
};
void LoanDraw(Graphics& graphics, std::function<void(DrawProxy&&)> f)
{
graphics.beginDraw();
f(DrawProxy{ graphics });
graphics.endDraw();
}
void test2(Graphics& graphics)
{
// LoanDrawがbegin/endDrawを呼んでくれる
LoanDraw(graphics, [](DrawProxy&& proxy) {
proxy.drawXXXX();
proxy.drawYYYY();
});
}
良くも悪くもスコープの外に処理を持ち出せないので、その必要のないケースでは安心です。
Template Methodパターン
継承で解決する方法も考えられます。
class DrawerBase
{
public:
explicit DrawerBase(Graphics& g) : mGraphics(g) {}
void draw()
{
mGraphics.beginDraw();
drawImpl_(); // Template Methodパターン
mGraphics.endDraw();
}
protected:
// drawImpl_から呼んでもらう用
// (ローンパターンにおけるDrawProxyのようにしてもいい)
void drawXXXX_() { mGraphics.drawXXXX(); }
void drawYYYY_() { mGraphics.drawYYYY(); }
private:
virtual void drawImpl_() = 0;
Graphics mGraphics;
};
void test3(Graphics& graphics)
{
class Drawer : public DrawerBase
{
public:
explicit Drawer(Graphics& g) : DrawerBase(g) {}
private:
void drawImpl_() override
{
drawXXXX_();
drawYYYY_();
}
};
Drawer{ graphics }.draw();
}
こちらは処理の数だけDrawerBase継承を増やさないといけないのがちょっと大がかりすぎますね。
まとめ
スコープ外持ち出し | 利用側の記述量 | |
---|---|---|
RAII | 〇 | 小 |
ローンパターン | × | 小 |
Template Methodパターン | × | 大 |
- スコープ外に持ち出す必要があるならRAII
- そうでなければローンパターンがオススメ
です。