2007-08-17

C++ と DI

Java と DI (Dependency Injection) の世界から C++ に戻ってくると気が滅入る. すべてがくっついている. ああ... "Working Effectively With Legacy Code" に従ってバリバリと依存を引き剥がすことになるんだけれど, もうウンザリ. せめて新たに書くコードはレガシー風味とさよならしたい. DI したい.

C++ にも少しは DI コンテナの実装がある. Autumn Framework とか. ただリフレクションのない C++ では DI コンテナを使う有難味が薄い. Autumn Framework のチュートリアルを見ると無力感に襲われる. 閉じた型システムの再発明. C++ の限界もあるだろうから, あまり責める気は起きない. COM のような既存のオブジェクトシステムに DI を載せることはできるかもしれない. けれど資産を活かせる事情がない限りオーバーヘッドが大きすぎる. できれば Plain Old C++ Object を使いたい.

コンテナのフレームワークがなくても DI の旨味は十分にある. 依存関係を外に押し出せるし, テストもできる. そこでフレームワークを使わない手動の DI について少し考えてみたい.

方針

DI にはいくつかのバリエーションがある. 大きな区分としては setter injection と constructor injection という 注入方法の違い. どちらかに絞る必要はないけれど, C++ の RAII 原則に揃えるなら constructor injection が自然. 基本はこちらに揃えつつ, 循環参照が必要などの事情に合わせて setter injection を併用すればよさそうだ.

そのほか C++ 固有の問題がある.

こうした点を踏まえつつ話を進める予定.

案 1: 実行時多態とスマートポインタ

まずは素朴に行こう. Martin Fawler の例をそのまま C++ に移植してみる. 寿命管理にはスマートポインタを使っておく.


 /* movie の値オブジェクト */
 class movie_t {
 public:
   movie_t(const std::string& t, const std::string& d)
     : m_title(t), m_director(d) {}

   // ...
 private:
   std::string m_title;
   std::string m_director;
 };

 /* movie finder インターフェイス */
 class movie_finder_t {
 public:
   typedef std::vector<movie_t> list_type;

   virtual ~movie_finder_t() {}
   virtual list_type find_all() const = 0;
 };

 /* 参照はスマートポインタで */
 typedef boost::shared_ptr<movie_finder_t> movie_finder_ptr;

 /* movie lister は具象クラス */
 class movie_lister_t : boost::noncopyable {
 public:
   typedef movie_finder_t::list_type list_type;

   /* constructor injection */
   movie_lister_t(movie_finder_ptr finder) : m_finder(finder) {}

   list_type find_directed_by(const std::string& director) const {
     list_type ret = m_finder->find_all(); // ここで finder を使っている
     // ...
     return ret;
   }

 private:
   movie_finder_ptr m_finder;
 };

 // ...

 /* movie finder の実装 */
 class colon_movie_finder_t : public movie_finder_t, boost::noncopyable {
 public:
   colon_movie_finder_t(const std::string& filename)
     : m_filename(filename) {}

   virtual list_type find_all() const {
     list_type ret;
     // ...
     return ret;
   }

 private:
   std::string m_filename;
 };

 /* アプリケーションはコンストラクタで接続を行う */
 class application_t {
 public:
  application_t(const std::string& filename)
     : m_lister(to_ptr(new movie_lister_t
                       (to_ptr(new colon_movie_finder_t(filename))))) {}

 // ...
 private:
   movie_lister_ptr m_lister;
 };

例では boost::shared_ptr を使っているけれど, COM のように侵入的な参照カウンタを使っているなら自身のスマートポインタを使ってもいい.

案 2: 実行時多態と寿命コンテナ

スマートポインタには循環参照を扱えない欠点がある. オーバーヘッドもある. 動的にメモリを確保するし, 参照カウンタの操作にはロックがいるかもしれない. それに世の中の C++ プログラマは案外スマートポインタ嫌いが多い. そこでスマートポインタを使わない方法も考えてみる.

多くのアプリケーションは固有のスコープをいくつか持っており, それに合わせてオブジェクト群を作ったり捨てたりする. ウェブサーバならリクエスト, ブラウザならページ, レンダラならフレーム, データベースならトランザクション... そのスコープ内にあるオブジェクトを管理するためのコンテナを作ろう. コンテナが一括して寿命を握り, コンテナ内のオブジェクトは寿命を気にせず素のポインタを使う. (APR のような Memory Pool の型付けバージョンだと思ってもいい.)

 class movie_lister_t {
 public:
   movie_lister_t(movie_finder_t* finder) : m_finder(finder) {}

   // ...
 private:
   movie_finder_t* m_finder; /* 素のポインタで OK */
 };

 //...

 /* 寿命コンテナ */
 class application_t {
 public:
   application_t(const std::string& filename)
     : m_finder(filename), m_lister(&m_finder) {} /* 接続 */

   //...
 private: /* 寿命がスコープに付随するオブジェクトを保持 */
   colon_movie_finder_t m_finder;
   movie_lister_t m_lister;
 };

なお, 循環参照があると コンストラクタの初期化子だけで接続を済ませることができない. オブジェクトを接続するコードをコンストラクタ内に書く必要がある. RAII が弱まるけれど仕方ない.

案 3: テンプレートによるコンパイル時多態

これまでの DI は実行時多態を使っているけれど, 実行時の多態がいつも必要とは限らない. Java でもテスト用のモックと実クラスの二種類しかないことは多い. どちらを使うかはコンパイルの時点でわかっている.

そんな時, C++ ならテンプレートによるコンパイル時多態を使うことができる. コンパイル時多態なら仮想関数のオーバーヘッドがないだけ速くなる. インライン展開も期待できる. 性能への影響は大きい.

 /* インターフェイスのかわりに型パラメタを使う */
 template<class MovieFinder>
 class movie_lister_t {
 public:
   typedef MovieFinder finder_type;
   typedef typename finder_type::list_type list_type;

   movie_lister_t(finder_type* finder)
     : m_finder(finder) {}

  // ...
 private:
   finder_type* m_finder;
 };

 /* MovieFinder の実装. インターフェイスクラスを継承しなくなった */
 class colon_movie_finder_t : boost::noncopyable {
 public:
   colon_movie_finder_t(const std::string& filename)
     : m_filename(filename) {}

   list_type find_all() { /* virtual でない */
     // ...
   }

   // ...
 };

 // ...

 /* 寿命コンテナに大きな違いはなし. */
 class application_t {
 public:
   typedef colon_movie_finder_t finder_type;
   typedef movie_lister_t<finder_type> lister_type;

   application_t(const std::string& filename)
     : m_finder(filename), m_lister(&m_finder) {}

   // ...
 private:
   finder_type m_finder;
   lister_type m_lister;
 };

案 4: 実体の埋め込み

Java は関連と集約を区別しない. オブジェクトは常に参照を通して扱う. C++ では関連をポインタ, 集約 (特にコンポジション) を実体として扱うことができる.

/* ポインタを使った関連 */
class Foo {
  Bar* m_bar;
};

/* 実体を使ったコンポジション */
class Foo {
  Bar m_bar;
};

実体を使うとポインタのぶんだけ空間効率がいい. ついでに寿命管理もシンプルになる. データベースの問合せ結果やメッシュの頂点のように大量に作られるオブジェクトでは できるだけ空間効率を上げたい.

手動 DI でも実体を使おう. movie finder の実体を movie lister に埋めこんでみる. (意味的にはおかしいけれど見逃してください.)

 template<class MovieFinder>
 class movie_lister_t {
 public:
   typedef MovieFinder finder_type;
   typedef typename finder_type::list_type list_type;

   movie_lister_t(const std::string& filename)
     : m_finder(filename) {}

   list_type find_directed_by(const std::string& director) const {
     list_type ret = m_finder.find_all(); /* "->" が "." に */
     // ...
     return ret;
   }

 private:
   finder_type m_finder; /* finder を実体で持っている */
 };

 // ...

 class application_t {
 public:
   typedef movie_lister_t<colon_movie_finder_t> lister_type;

   application_t(const std::string& filename)
     : m_lister(filename) {}
   // ...
 private: /* finder を持たなくなった */
   lister_type m_lister;
 };

案 5: コンストラクタ引数の制約

実体の埋め込みには面倒なことがある. 埋めこまれたオブジェクトのコンストラクタにはどうやって引数を渡そう. 上の例では適当にごまかしたけれど, たとえばモックと実クラスで引数の型や個数が違うと困る. テスト用 mock_movie_finder_t の コンストラクタには find_all() で返す値のリストを渡したい, なんてことはある.

仕方ないのでクラスに制限をつけよう. DI するクラスはコンストラクの引数を ctor_type 型として 公開しなければいけないことにする.

 class colon_movie_finder_t : boost::noncopyable {
 public:
   typedef std::vector<movie_t> list_type;
   typedef std::string ctor_type; /* コンストラクタ引数の型を公開 */

   colon_movie_finder_t(const ctor_type& ctor)
     : m_filename(ctor)
   {}

   // ...
 };

concept で書くとこんなかんじ. (たぶん.)

 concept ConstructorInjectable<typename T> {
   typename ctor_type;
   T::T(const ctor_type&);
 };

結局こんなふうになる:

 /* 別の ConstructorInjectable クラス */
 class rental_status_database_t : boost::noncopyable {
 public:
   typedef std::string ctor_type; /* ConstructorInjectable の要件 */

   rental_status_database_t(const std::string& uri)
     : m_uri(uri) {}

   // ...
 };

 /* レガシー用ユーティリティ. 特殊化して ctor_type を定義できる */
 template<class T> struct ctor_type_of {
   typedef typename T::ctor_type type;
 };

 template<class MovieFinder, class RentalStatus>
 class movie_lister_t {
 public:
   typedef MovieFinder finder_type;
   typedef RentalStatus rental_status_type;
   typedef typename finder_type::list_type list_type;
   typedef boost::tuple< typename ctor_type_of<finder_type>::type,
                         typename ctor_type_of<rental_status_type>::type > ctor_type;

   movie_lister_t(const ctor_type& ctor)
     : m_finder(boost::tuples::get<0>(ctor)),
       m_rental_status(boost::tuples::get<1>(ctor)) {}

   // ...
 private: /* コンポジションは実体のまま */
   finder_type m_finder;
   rental_status_type m_rental_status;
 };

 // ...

 /* 寿命コンテナは実質的に手書きの DI コンテナになる */
 template<typename MovieFinder, typename RentalStatus>
 class basic_application_t {
 public:
   typedef MovieFinder finder_type;
   typedef RentalStatus status_type;
   typedef movie_lister_t<finder_type, status_type> lister_type;
   typedef typename ctor_type_of<lister_type>::type ctor_type;

   basic_application_t(const ctor_type& ctor)
     : m_lister(ctor) {}

   // ...
 private:
   lister_type m_lister;
 };

 typedef basic_application_t<colon_movie_finder_t,
                             rental_status_database_t> application_t;

 // ...

 /* コンテナのインスタンス化はこんなかんじ */
 sc::application_t app(sc::application_t::ctor_type
                       ("foo.txt", "local://foo/"));

ctor_type が再帰的に使われているのがわかると思う.

この方法にも制限はある. まず複数の引数を渡すことができない. 適当なオブジェクトに詰めて渡す必要がある. (上の例では boost::tuple を使った.) またコンストラクタをオーバーロードすることもできない. 個人的にはそう理不尽な制限ではないと思う. どうだろね.


総称プログラミングとしての DI

C++ に慣れた人なら, 最後の例は単なる総称プログラミング, 特に Alexandrescu 風 policy 様式の亜種に見えるかもしれない. その感触はおよそ正しいと思う. 実装同士の結びつきを遅らせてインターフェイスとプログラミングをする, そんな手口への合意がある. その実現手段として Java は DI を, C++ は GP を選んだ. 意図に大きな違いはない.

ただ総称性を強調したせいか, C++ の GP は多くのプログラマにとって敷居が高くなっている気がする. インターフェイス主導のプログラミングをする方法の一つとして GP を捉えなおし, 気楽に使えばいいと思う.

コンパイル時間の制限から全てのコードを GP 風 DI で組み立てるのは現実的でない. 大きなブロックは実行時多態で組み立て, 性能を要求される部分はコンパイル時多態でチューンする. 既存の C++ コードで広く使われているこうしたやりかたは DI になっても変わらない.

まとめ: