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:この段落は公開後に追記した。

Practical Object-Oriented Design in Ruby 読書メモ

オブジェクト指向設計は、品質の高いソフトウェアを開発するためにはどう設計すべきか、という問いに対するひとつの解。適切にグルーピングされたオブジェクト同士が適切な相手とのみメッセージをやりとりする。相手が誰であるかよりも、自分が出したメッセージに相手が反応できることを重視する。オブジェクトは基本的に相手の詳細を知ろうとしないし、自分のことを不必要にさらけ出さない。

以下、Practical Object-Oriented Design in Ruby (Sandi Metz) を読んで。

How Design Fails

プログラマーは設計について知らなくてもプログラムを書けてしまうが、規模が大きくなるとそのプログラマーは「ええ、その機能を追加することはできますが、それによって何もかも壊れてしまいます」という状態になる。

設計について知ってはいるが、適切な適用方法を知らないプログラマーはオーバーデザインしてしまう。パターンがないところにパターンを見出し、*いしのなかにいる* になってしまい、「いえ、その機能は追加できません。これはそういうことをするようには設計されていないんです」となってしまう。

設計とプログラミングが隔離されていても失敗する。設計段階での誤解がコードにそのまま残る。“専門家”が設計したアプリケーションを書かされるプログラマーはこう言うだろう。「なるほど、確かにこれを書くことはできますが、これはあなたが本当に欲しいものではないですし、後悔することになりますよ」

オーバーデザインはつい先月経験した。永続化が必要ないところにリポジトリを作ろうとしてしまい、リポジトリの書き込み/読み出しを書こうとして「なんだか複雑すぎる」と感じたあたりで永続化が不要なことに気づいた。また、ストラテジーパターンで済むところにファクトリーを作ろうとしたりもした。なんならストラテジーも不要かもしれないが、僕のケースでは依存性をどこに埋め込むかという判断と、依存性を分離することで得られる抽象化とコードの複雑さのトレードオフの問題だった。

「設計とプログラミングが隔離されていても失敗する」ことについては、 Eric Evans の『ドメイン駆動設計』(DDD) が代表的な文献だろう。DDDは分量こそ多いものの読みやすいので気軽におすすめできる。DDD はメンテナンスしやすいコードを書くためのひとつの設計手法で、コードにそのまま埋め込めるような設計をめざす。

僕がいま書いているコードは基本的に僕しか使わない、大学院での研究のためのコードなので、あまり設計について拘らなくても問題はないかもしれない。実際、ほとんどの同期や先輩、後輩は設計についてまったく気にしていない。ひとりだけ、オブジェクト指向デザインパターンを勉強してコードを書きまくっている優秀な同期がいる程度。

しかし、研究室に数年間だけ在籍する学生こそ、オブジェクト指向などの設計を身に着けたほうがいいと僕は思う。研究室にもよるが、うちの研究室では研究は基本ひとりでやり、先輩の研究の続きをやることが多い。すると、自分のコードを数年後に読み直すハメになる後輩がいるかもしれない。雑に書かれたレガシーコードに向き合うのはかなりキツい。プログラミングに慣れていない学生にとって他人のコードを読み解くのは(設計がきちんとしていようがいまいが)大変な作業だし、研究の本質的な部分ではないので解読やリファクタが進んでも「進捗が出ていない」感が拭えない(少なくとも自分はそうだった)。*1

単一責任の原則

オブジェクト指向システムの核心はメッセージだが、最も見えやすいのはクラスだ。あとで変更しやすいクラス設計にしておけば、現在と未来の両方で利益がある。「変更しやすい」とは、たとえば次のように定義できる。

  • 変更の副作用がない
  • 要件の小さな変更が、ふさわしい小ささの変更をコードに及ぼす
  • 再利用しやすい
  • 変更する最も簡単な方法が、「それ自体変更しやすいコード」を加えることである

であれば、コードはTRUEであるべきだ。

  • Transparent 変更するコードと、それに依存するコードに関して、変更の結果が明白である。
  • Reasonable 変更のコストが利益に比例している。
  • Usable 既存のコードがまったく別のコンテキストでも使える。
  • Exemplary コードを変更する人がTRUEなコードを書くよう、コード自身が促す。

未来に起こりうる事件を防ぐためにいまコストを支払うべきか?これは個々のケースごとに現在の利益と未来の利益のトレードオフを計算すべき問題だ。

データ構造ではなく振る舞いに依存してコードを書くことも、変更しやすいクラス設計で重要だ。

SOLID 原則の S、単一責任。クラスが単一責任であると保証することが、TRUEなコードを書く第一歩である。クラスに目的外のメソッドやメンバ変数がいるべきではない。

OO設計がまったくわかっていない時期には、手続きっぽい書き方で、見えていないはずのデータ構造にめちゃめちゃ依存するコードを書いてしまう、というのをとてもたくさんやった。オブジェクトを設計せずに、配列やコンテナにインデックスでアクセスしまくった。データ構造に直接手をつっこむコードは一夜にして壊れるし、変更を難しくするので絶対にやめたほうがいい。

OO設計を徹底するとコード量はまず増えるが、それは適切な抽象化を行うために必要な増加であって、ひとつのクラス内で使うアルゴリズムだけに限れば、短くてきれいなコードほど正しく動く気がするし、少なくともメンテしやすい。

依存の根を張らない

依存性は、他のクラスの名前、他のクラスに送るメッセージの名前、メッセージの引数、引数の順番のどれかをオブジェクトが知っているときに存在する。依存は少ないほどよい。

他のクラス名を直接書くことを避けるには、Dependency Injection (DI) が使える。DI が使えないときには、ラッピングするなどして依存を隔離することで影響範囲を最小限に抑えよう。

依存関係は、より変化しやすいクラスからより変化しにくいクラスへ張られるべきである。具体クラスより抽象クラスに依存するほうが安全だ。

抽象クラスに依存すべき、というのは GoFデザインパターンを眺めてなんとなく感じた。

C++ で DI しようとすると結構苦労するので、最近は普通にテンプレートを用いたジェネリックプログラミングをしている。IDEでクライアントコードを書くまで間違いに気づけないのがアレだが…(多分IDEの使い方が間違ってるんだと思う)

柔軟なインターフェース

  • 「どうやってほしいか」を伝える代わりに「何がほしいか」を伝える
  • 文脈に依存しない
  • 他のオブジェクトを信頼する
  • メッセージをつなぎまくらない(デメテルの法則)

僕はまだ油断すると手続き的にシーケンスを設計してしまい、インターフェースも文脈依存になってしまう。他のオブジェクトを信頼すれば、文脈依存性を減らせる。「『どうやってほしいか』を伝える代わりに『何がほしいか』を伝える」のは結構大事で、無駄な依存をへらすのに役立つ。

ダックタイピングでコストを減らす

ヒルのように振る舞うなら、それはアヒルである。あるオブジェクトのユーザーにとって、大事なのはそのオブジェクトのクラスではなく、インターフェースである。ダックタイピングを使うことで、具体クラスへの依存を減らせる(なお抽象クラスを実装する必要はない)。多態性はダックタイピングや継承・トレイト(Ruby ではモジュール)によって実現できる。

C++ のテンプレートはまさにダックタイピングと言える。

継承とトレイトとコンポジション

継承は抽象的なクラスを拡張する。しかし継承では、子クラスが複数の親クラスを持てない。複数の親クラスがほしいときはトレイト(Ruby ではモジュール)を使おう。色々なクラスを足し合わせても不足するときはコンポジションを使う。

テスト

テストは必要不可欠である。テストがあればなんでもできる。

cf. Kent Beck TDD

振り返り

継承の章以降は斜め読みだが、 Bertrand Meyer の鈍器よりもはるかに薄く、言葉遣いが平易で読みやすい本だった。逆に Meyer はなぜあんなに堅苦しい文体なんだ。

動的型付けもやっぱりいいよな、と思える本だった。簡潔に書けることは良いことだ。静的型付けな言語でも簡潔に書けるテクニックをもっと学んでいこう。

*1:そうは言っても大学は専門学校ではないから、プログラミングのノウハウを教えるのは個々の研究室や学生自身の仕事だろう。教授や助教はプログラミングやOO設計を教えられるほど暇ではないし、それは本職ではない。この前提に立つと、研究室で書くコードは可能な範囲でOSSにして卒業後もメンテできるようにしたり、そもそもできるだけコードを書かずに問題を解決するのがいいかもしれない。研究室のほとんどの学生に、コードのバージョン管理やソーシャルコーディングという概念がないのももったいない。せっかく研究室という一つのまとまりになっているので、CI/CDの仕組みを教えるほうが有益かも。そうなると git から教える必要があるので教育コストはかかる。ただまあ、うちの研究室の卒研生はCLIには慣れているのでなんとかなりそう。