1. オブジェクト指向設計
- オブジェクト指向は、オブジェクト間でメッセージが受け渡される世界観
- オブジェクト指向設計とは、依存関係を管理すること
- 実用的な設計は、アプリケーションの将来に何が起こるかを予測することではなく、何か変化が起こることを認めること
- 変更コストの削減
2. 単一責任のクラスを設計する
変更の簡単さの定義
- 変更は副作用をもたらさない
- 要件の変更が小さければ、コードの変更も相応して小さい
- 既存のコードは簡単に再利用できる
- 最も簡単な変更方法はコードの追加。ただし追加するコードはそれ自体が変更が容易なものとする
- 単一責任が重要な理由は、いくつもの責任が絡み合った状態だとそのクラスを変更する理由がいくつも生じてしまい、目的の用途とは関係ない理由で変更が加わってしまうので、そのクラスに依存するすべてのクラスを破壊する可能性があるから
- 単一責任の見極め方
- クラスの持つメソッドを質問に言い換えたときに、意味をなす質問になっているかどうか
- e.g.
- Gear さん、あなたの比を教えてくれませんか? -> 意味通じる
- Gear さん、あなたのタイヤのサイズを教えてくれませんか? -> 意味わからん
- e.g.
- クラスを一文で説明できるかどうか。「それと」、「または」が含まれてしまうならば複数の責任を持ってしまっている
- クラスの持つメソッドを質問に言い換えたときに、意味をなす質問になっているかどうか
- データではなく振る舞いに依存させるために、変数はそれらを定義しているクラスからでさえも隠蔽する
- 変数は公開せず、メッセージを送るようにする
- 同様に、複雑なデータ構造も隠蔽する
- Ruby なら Struct クラスを使うことで、外部データの構造の変化に強くなるし、構造がわかるのでコードの可読性も向上する
- メソッド自体も単一責任にする
- 細かく分けすぎでは?とも思ったが、その過程でクラスの責任を明確にする効果があるのは大きいと思う
- また、複数の責任を持つメソッドにコメントを残すことが多い。単一責任のメソッドになればメソッド名がコメントの目的を果たすので、これも良い副作用だと感じた
- 再利用しやすい
- 小さなメソッドなら他のクラスへの移動もかんたん
- クラス内の余計な責任を隔離する
- クラス内の責任を整理していった結果、別クラスとして切り出せる振る舞いがあったとする。しかし、現実には何らかの理由で別クラスとして「今すぐ」に切り出すことは避けたい場合もある。たとえば、まだプロダクトの方向性が定まっていなくて考えが変わる場合があって、アプリケーション全体で利用可能なクラスとして作ってしまうと、他のクラスが依存してしまいかねないのを避けるため。
- そこで、Struct ブロックに構造だけでなく振る舞いも切り出すことで、クラスからは「ひとまず」責任を取り出しやすい状態にはしておける。つまり、決定を先送りにすることができる。
- Struct の使いどころが上手いと思った。データ構造を隠蔽するために使うという考えはなかったので、ぜひ使っていきたい。
3. 依存関係を管理する
- 複雑な問題を解決するために複数のオブジェクトで共同作業をすることになる。このとき、オブジェクト同士は「知っている=依存」の状態を作り出す。
- オブジェクトの依存関係の定義
- 他のクラスの名前を知っている
- self 以外にメッセージを送ろうとしている
- メッセージが要求する引数を知っている
- その引数の順番を知っている
- 一定の依存関係があることは、複雑な問題を解決するために仕方のないこと
- 不必要な依存があることが問題
- オブジェクト間の依存が強固、つまり結合を作り出してしまうと、それらは1つのユニットであるかのように振る舞うことになる。こうなると再利用することが難しくなるし、1 つの変更が依存するすべてのオブジェクトに影響してカオスを生む。
- 依存オブジェクトの注入(Dependency Injection)
- Ruby でも DI は可能
- クラス名を知っておく責任、そのクラスに送るメソッド名を知っておく責任がどこか他のクラスに属するものではないかと疑える能力
- 2 章と同じ要領で、依存を隔離しておいて、必要な場面で依存を特定・削減できるようにしておく
class Gear
attr_reader: :wheel
def wheel
wheel.diameter
end
end
- option hash を受け取るようにすることで、引数への依存をなくす
- デフォルト値を設定する。
- 複雑な値が必要なら
defaults
メソッドを定義する
- 複雑な値が必要なら
- Factory Pattern
- 自身より変更されないものに依存方向を向ける
- 変更の起きやすさを軸にする
- 抽象化されたクラスは具象クラスより変更されにくい。抽象クラスへの依存は具象クラスへの依存よりも安全。
4. 柔軟なインターフェースを作る
- オブジェクト間のメッセージに意識を向ける
- クラスが何を「する」ではなく、何を「明らかにする」かが問題になる
- インターフェースも様々な概念がある
- パブリックインターフェース → クラス内にある、他のオブジェクトから使われるようなメソッドがそのクラスのインターフェースを構成する
- 静的言語における構文としてのインターフェース
- パブリックインタフェース
- 他社が依存しても安定する
- アプリケーションのユースケースを整理するためにオブジェクトのメッセージの両方を明らかにする必要がある。そのときに役立つツールがシーケンス図。
- ユースケースの名刺がオブジェクトで、アクションがメッセージになる
- シーケンス図を書くと、設計の重点がクラスからメッセージになる。
- シーケンス図によっておパブリックインタフェースをあらわにする
- メッセージに基づく視点は、クラスに基づく視点よりも柔軟なアプリケーションを生み出す
- 「このメッセージを送る必要があるけれど、だれが応答すべきなんだろう」
- オブジェクトが存在するからメッセージを送信するのではなく、メッセージを送信するためにオブジェクトが存在する
- 「どのように」を伝えるのではなく「何を」を頼む
- インターフェースのサイズ(数)が小さくなると、依存が減るので安定する
- オブジェクトの要求するコンテキストが単純なほど、再利用しやすい
- DI を使うことでコンテキストを単純にしやすい
- ほかのオブジェクトを手放しで信頼することが、オブジェクト指向設計の要。
- オブジェクトからメッセージに注意を切り替えて、パブリックインターフェースの上に作られたアプリケーションを設計しよう
- インターフェースを意図をもって作ることこそが、テストよりも他のどんなコードよりも、アプリケーションの将来性を決める重要なこと
- public, protected, private は不安定さを伝える手段
- public は依存される前提で作られるので、変更が慎重になるので安定する
- private はクラス内でのみ使用されて、影響範囲が小さいので比較的自由に変更できるから安定度が低い
- protected は public と private の中間。
- ほかのクラスのプライベートメソッドの使用を矯正するなら設計を考え直すべき
- どうしても依存しなければならないときは、依存を隔離する
- デメテルの法則
- 隣の隣のオブジェクトにメッセージを送ってはいけない
- 必ず守るべき法則というわけではない。変更コストと頻度、違反を修正するコストのバランスを取ること
- 違反が見つかったら、パブリックインターフェースが欠けているオブジェクトがあるのではないか、というヒントになる
5. ダックタイピングでコストを削減する
- クラスはオブジェクトがパブリックインタフェースを獲得するための 1 つの方法
- オフジェクトが何で「ある」かではなく、何を「する」かが重要
- クラスをまたぐ型、つまりダックタイプのパブリックインターフェースは契約を表す
- ダックタイプの実装は簡単。難しいのはダックタイプが必要であると気づくこと、そのインターフェースを抽象化すること
- 設計の目的はコストを下げること。ダックタイプを実装することでこの目的が達成できるならやればいいし、特に達成できないならやる必要はない
- ダックタイプはコードが安全に依存できる安定した抽象を明らかにする
6. 継承によって振る舞いを獲得する
- あなたがだれなのか知っている。なぜなら私はあなたがすることを知っているのだから
- この状態は依存を表しており、変更のコストを上げる
- 継承は、共通の振る舞いを持つものの、いくつかの面においては異なるという、強く関連した型の問題を解決する
- 継承の問題点
- いらない振る舞いも継承してしまう -> 親クラスに余計なものが混じっている、抽象化ができていないということ
- 継承のルール
- オブジェクトが汎化ー特化の関係を持っていること
- 正しいコーディングテクニックを使っていること
- サブクラスがたった 1 つだけの状態で抽象クラスを見つけたとしても、抽象クラスとして切り出すべきではない。具体的な要求が出てくるときまでは、今の状態で十分。また、仮に 2 つ見つかったとしても「まだ」継承を実施するのには適切な時期ではないかもしれない。
- 階層構造を作るのはコストがかかる
- サブクラスが 3 つなら、かなりの情報を提供してくれるので、正しい抽象を見つけられる可能性が格段に向上する
- 継承を利用した階層構造を作るのと、重複したコードをコピペしたクラスを作るかどうかの選択は、「あとどれくらいで 3 種類目のサブクラスが登場するか」と「複製のコストがどれだけあるか」の見積もりによって異なる
- 設計の戦略を決める際、一般的に有用な方法
- 「もし間違っていたら、何が起こるだろう」と考える
- 具象から抽象を見つけて昇格させる
- 抽象に具象的な振る舞いが残らないように
- いったん、具象からはじめて抽象を見つける
- 抽象から具象への降格失敗は影響が大きい
- 抽象に具象的な振る舞いが残らないように
- 設計者の決断に伴う 2 つのコスト
- 実装コスト
- 間違いだとわかったときの変更コスト
super
の送信があると、親のことを知っている状態になっている- サブクラスがアルゴリズムの知識を持った状態
- フックメッセージを使ってサブクラスを疎結合にする
- フックメッセージを用意して実行タイミングの制御をスーパークラスに任せる
- サブクラスはそれに合致するメソッドを実装し情報を提供する
- サブクラスからアルゴリズムの知識を取り除く
- 親を信頼する
- フックメッセージを使って super(親)を知らずにできないか考える
7. モジュールでロールの振る舞いを共有する
- 継承
- 〜である(is-a)
- 親と子という関係を定義する
- モジュール
- 〜のように振る舞う(behaves-like-a)
- 複数のクラス間で振る舞いを共有する
- モジュールを使ったコードの書き方と継承を使ったコードの書き方は似る
- 抽象に固執する
- 親クラス内のコードを使わない子クラスがあってはならない
- モジュールも同様
- 親クラス内のコードを使わない子クラスがあってはならない
- 契約を守る
- 子クラスは親クラスと置換できることを約束する
- リスコフの置換原則
- 子クラスは親クラスと置換できることを約束する
- super は依存を生むので可能な限り避ける
- 階層構造は浅くする
8. コンポジションでオブジェクトを組み合わせる
- 継承からコンポジションへ
- コンポジションとは、個別の部品を複雑な全体へと組み合わせる行為
- 部品が増えると、必要な部品とその作り方を知る必要が出てくる
- Factory Pattern
- OpenStruct
- クラスによる継承は「コード構成のテクニック」
- オブジェクトの階層構造に構成するコストを払う代わりに、メッセージの委譲は無料で手に入れられる
- コンポジションはこのコストと利点を逆転させる代替案
- コンポジションは独立して存在できるが、代わりにメッセージの委譲コストを払う必要がある
- 優先度はコンポジション>継承
- コンポジションが持つ依存は継承が持つ依存より小さいから
- 継承を採用するケース
- 低いリスクで高い利益を生み出すとき
- 正しくモデル化された階層構造ならば、「オープンクローズド」なコードを得やすい
- is-a -> 継承
- behaves-like-a -> ダックタイプ
- has-a -> コンポジション
9.費用対効果の高いテストを設計する
- テストの目的は、設計と同じく変更のコストを削減すること
- テストのしづらいコードは設計がまずい証拠
- 何をテストすれば良いのか
- 安定したパブリックインターフェースに対するテスト
- オブジェクトの境界に入ってくる(受信する)か、出ていく(送信する)メッセージに集中する
- オブジェクトの内部を意図的に無視する
- パブリックインターフェースに定義されるメッセージを対象としたテスト
- 受信メッセージはその戻り値の状態をテストする
- 送信コマンド(副作用を持つ)メッセージは送り手側でテストを書く
- メッセージが送信された回数、使われた引数など
- 送信クエリ(副作用を持たない)メッセージは送り手でテスト不要
- 受け手側でテストを書く
- テストを最初に書いたオブジェクトははじめから多少の再利用性がある
- 初級の設計者はテストファーストでコードを書いたほうが有益
- プライベートメソッドは無視する
- パブリックメソッドのテストで実行されているはずだから
- 大量のプライベートメソッドを持っているオブジェクトは責任を持ちすぎている可能性がある
- プライベートメソッドをテストしない勇気がないのなら新たなオブジェクトに切り出すのも方法としてある
- ただし、本当に役に立つのは新しいインターフェースが確かに安定しているときだけである
- プライベートメソッドは不安定なものであるから、信頼性が増すことはない
- リファクタリングを経て、安全性が増したのであれば別クラスに出してもよい
感想
オブジェクト指向設計は「単一責任のクラスを作っておけばええんやろ」という程度にしか理解していなかったが、実際にはクラス間のメッセージのやりとりこそが主要な設計要素なんだと理解した。
次はドメイン駆動設計関連の本を読んでみるかな