閑古鳥

オールドプログラマの日記。プログラミングとか病気(透析)の話とか。

NonVirtual Interface パターン

色々な図形を描画するアプリケーションを開発しているとします。この図形を描画する処理を、まず抽象クラス Shape を用意し、それから実際の描画処理を派生クラスで実装しようと思ったとき、以下のような実装を行いました。

class Shape
{
public:
  Shape() {}
  virtual Shape() {}

  //! 描画処理
  virtual void Draw() = 0;
};

class Rectangle : public Shape
{
public:
  virtual void Draw()
  {
    // ... 矩形を描画する ...
  }
};

まず矩形を描画する Rectangle クラスを実装しました。この Rectangle クラスは、矩形の描画処理のほかに、画面に描画する前の下準備も一緒に行っていました。たとえば、開発環境は Windows でしたので、 GDI のペンなどを作成する、といったことです。

もちろん、描画するオブジェクトは矩形だけではなく、円や線分など様々なものが追加されていきます。このとき、円や線分の各クラスの Draw メンバ関数でも、 Rectangle クラスの Draw メンバ関数で行った「下準備」を行わなければなりませんが、これらの処理は全く同一のコードなので、全てのクラスで定義することは避けたいところです。

そこでそれらの下準備を行うためのメンバ関数を別に作成することにしました。先に述べたとおり、どの具象クラスでも行う事は同じなため、この関数は抽象クラス (Shape) に非仮想関数 (つまり通常のオーバーライドを行えない関数) として実装しました。

class Shape
{
public:
  Shape() {}
  virtual Shape() {}

  //! 描画処理
  virtual void Draw() = 0;
private:
  //! 描画前の処理
  void Prepare();
};

class Rectangle : public Shape
{
public:
  virtual void Draw()
  {
    // 描画を行う前の処理
    Prepare();
    // ... 矩形を描画する ...
  }
};

Prepare メンバ関数に「下準備」を抽出したことで、コードの重複は避けることができました。しかし、今後また新たな図形を追加することになったときに、その図形クラスの Draw メンバ関数を定義するときに毎回、 Prepare メンバ関数を呼び出さなければならない、という作業が付帯することになりました。これは、避けられないものなのでしょうか?

最近、この問いについてひとつの答えが出せました。それは、 Draw メンバ関数自体は、 Shape クラスにのみ定義される非仮想関数として、具象クラスに公開する仮想関数は別に用意する、というものです。コードにすると、以下のようなイメージ。

class Shape
{
public:
  Shape() {}
  virtual Shape() {}

  //! 描画要求
  void doDraw()
  {
    Prepare();
    Draw();
  }
  
private:
  //! 実際の描画処理
  virtual void Draw() = 0;
};

class Rectangle : public Shape
{
private:
  virtual void Draw()
  {
    // ... 矩形を描画する ...
  }
};

この変更により、具象クラスは各々の図形を「描くだけ」で良くなり、その前(もしくは処理後)にどういった処理を行う必要があるのかを一切関知しなくて良くなります。新たに機能を追加する際にも実装漏れが起こりにくくなり、変更にも強そうです。

最近この仕組みを思いついて、いつか実践してみようと思っていたのですが、昨日 C++ Coding Standard を読んでいたら、似たようなことが書いてありました。どうやらこの手法には NonVirtual Interface(NVI) という名前が付けられているそうです。この本には NVI パターンのとして、上記のような手法に合わせ、外から仮想関数を半端に呼び出せないよう private に置く事を推奨していました。これに倣い、自分も今後は仮想関数はまず、 private に組み込むということを心がけようと思いました。