C++ で依存性をどこで注入するか

オブジェクト指向設計において、オブジェクト間の依存をコードの奥深くに埋め込むのは得策ではない。しかし同時に、誰から見て依存関係がはっきりとわかるべきかは、設計者が判断すべきことである。C++ において、オブジェクトのユーザーが依存関係を見ていいなら、テンプレートでいいだろう。オブジェクトの設計者だけが依存関係を見るべきなら、インターフェイスを使うべきだろう。

インターフェイスか、テンプレートか

C++ で、あるクラスの実装において、他のクラスの名前を具体的に書くことを避けるためには、例えばインターフェイス(のようなもの)や、テンプレートといった対処法がある。

テンプレートを用いたジェネリックプログラミング

C++ のテンプレートは、引数の具体的な型を書くことなしにクラスや関数を宣言・定義することを可能にする。POODRの記事で触れたダックタイピングのひとつでもある。

次のようなコードでクラスを宣言したとする。

template<class loader_t>
class CelestialObject {
public:
  CelestialObject(loader_t* loader);
};

すると、このクラスは次のようにして使える。

auto config_loader = new LoadConfig();
auto perseus_cluster = new CelestialObject<LoadConfig>(config_loader);

この CelestialObject<LoadConfig>() のように、 <型> と指定することで、上の例で言えばソースコード中の loader_tコンパイル時に具体的な型名に置き換えられて、実際に使われるコードが生成される。

クラステンプレートを使うと、しばしば入れ子のテンプレート表記ができる。たとえば次のような定義も書くことができる。

std::vector<std::map<std::string, std::map<std::string, double>>> data;

ここまで来るとさすがに新しくクラスをつくったほうがいいだろうが、このようにテンプレートが入れ子になるとコードは長くなり、あまり見た目はよくない。

また、クラステンプレートを1箇所で使うと、そのクラスを受取るクラスでもテンプレートを使うか、後述のインターフェイスを用いる必要が出てくる。これはテンプレートクラスの型が、具体的な <型> を指定して使用されるまで決まらないことによる。上の例で言えば、 CelestialObject ではなく CelestialObject<LoadConfig> まで決まって初めて型として指定できる。何らかの基底クラスを継承した場合は、基底クラスへのアップキャストが機能する。*1

ところで、テンプレートに具体的な型を入れて使うコードが見えるのは、たいていクラスのユーザーなので、テンプレートを用いると依存関係を見るのは各クラスのユーザーということになる。これは必ずしもデメリットではなく、ユーザーが自由に依存性を注入できるという意味ではとても自由度の高い設計を実現できるというメリットもある。というかメリットが非常に大きい。STLはこれによって成り立っている。

継承によるインターフェイス

上の例の LoadConfig クラスを、あるインターフェイスクラスの派生クラスとすれば、インターフェイスクラスを介して渡すことができる。

class LoadConfig: public ILoader {};

クラスのユーザーは次のように基底であるインターフェイスを指定する。

class CelestialObject {
public:
  CelestialObject(ILoader* loader);
};

すると次のようにアップキャストによって派生クラスを渡すことができる。

auto loader = LoadConfig();
auto cygnus = CelestialObject(loader);

このコードの loader の型は LoadConfig* だが、LoadConfig は基底として ILoader クラスをもっているので、 CelestialObject のコンストラクタに渡すことができる。このように子クラスではなく親クラスへ依存を張ることは、POODR やデザパタにあるように、より変更に強い設計を実現するための基本だ。

ところで、インターフェイスを用いた依存性注入が行われるのは、クラスの設計者側である。依存関係をクラスのユーザーに見せないことが良い場合もあるだろうし、一方でユーザーが自由に依存性を注入できないことがデメリットになる場合もあるだろう。

結局どっちがいいのか

インターフェイスとテンプレートのどちらかが絶対に良いということはなく、個々のケースで設計者がどう判断するかによって取るべき方法が自然と決まってくるだけだろう。研究用に自分だけが使うコードならテンプレートを存分に使えるだろうし、フレームワークやツールキットなどの他人に使ってもらうコードならインターフェイスが良いかもしれない。

ただまあ、インターフェイスを使うとそのぶんクラスが増え、コード量も増える。一方でテンプレートを使うとヘッダファイルとソースファイルの分離ができなくなる。

個人的には見た目がいまいちでも楽なのでテンプレートを使っている。

*1:この段落は公開後に追記した。