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には慣れているのでなんとかなりそう。