決意表明
1か月ちょい~2か月かけて少しずつ読み進めたい。
自分用のメモです。
※ 2020-07-05 冗長になってしまったので、一通り読み終わったらサンプルコードを中心にさらに要約する。
要点
オブジェクト指向プログラミングの概要・導入
1. オブジェクト指向設計
1.1 設計の賞賛
- オブジェクト指向設計の手法に従えば、コーディングを楽しくし、かつ最も効率良くソフトウェアを生産できる!
- オブジェクト指向設計 = 依存関係を管理すること。
- 部品
- オブジェクト
- 相互作用
- メッセージ
- 部品
- 設計の目的は変更コストの削減。
1.2 設計の道具
- 設計原則『SOLID』
- デザインパターン『GoF』
1.3 設計の行為
- BUFD(ウォーターフォール?)とオブジェクト指向設計とでは"設計"の意味が異なる。
- BUFD
- 未来の内部動作を特定した、完全な文書化。
- オブジェクト指向設計
- 変更を前提とした、小さな領域におけるコード構成。
- BUFD
1.4 オブジェクト指向プログラミングのかんたんな導入
- 手続き型言語
- 変数が持つデータ型は一つ。
- データと振る舞いは別物。
- オブジェクト指向言語
- オブジェクトは振る舞いを持つ。
- いくつもの型(=クラス)を持つ。
- オブジェクトの振る舞いを予想できる。
- オブジェクト指向言語は型を拡張できる。よって、オブジェクト指向アプリケーションはプログラマーの扱う特別独自のプログラミング言語となっていく。
クラスに焦点を当てる
2.単一責任のクラスを設計する
- アプリケーションのモデル化という目標には2つの異なる基準がある。
- クラスを使い、「いますぐに」求められる動作を行う。
- 「あとにも」かんたんに変更できるようにする。
- プログラミングの技術力が如実に現れる部分。
2.1 クラスに属するものを決める
- TRUEなコードを書くための第一歩は、単一の責任(=最小で有用)を持つよう徹底すること。
2.2 単一の責任を持つクラスを作る
- すべてのクラスやメソッドは、単一の責任を持つようにする。
- 単一責任とは?
- クラスの持つメソッドを質問に言い換えたときに、意味をなす質問になっている。
- 一文でクラスを説明できる。
- 「それと」や「または」が含まれない。
- 単一責任とは?
2.3 変更を歓迎するコードを書く
- インスタンス変数やデータ構造は隠蔽する = 振る舞い(メソッド)へと変更する。
- 予期せぬ変更がコードに影響を与えることを防ぐ。
- いったんすべてのメソッドを単一責任にしてしまうことで、クラスのスコープが明白になる。
3.依存関係を管理する
- オブジェクトの振る舞いの種類
- クラス自身が独自に実装すべき振る舞い
- 振る舞いの継承
- 振る舞いが他のオブジェクトに実装されている
3-1. 依存関係を理解する
-
依存関係が存在するのは、オブジェクトが次のものを知っている時。
- 他のクラスの名前
-
self
以外の他のオブジェクトに送られるメッセージの名前 - メッセージが要求する引数
- それらの引数の順番
-
コードの変更が、他のオブジェクトの変更を強制するような大がかりなものにならないように、依存を最低限にする。
3-2. 疎結合なコードを書く
- 依存は悪。
- 依存というのは全て、クラス(の設計)をむしばもうとする外来のバクテリアのようなもの。
-
他のクラスの名前がある時
- 依存オブジェクトの注入
- 他のオブジェクトのクラス名を知る必要がなくなり、結合の切り離しを実現できる。
- 依存するものを常に気に留め、それらを注入することを習慣化させていけば、クラスは自然と疎結合になる。
- 依存の隔離
- 他のクラスのインスタンス変数の作成を
self
の初期化時から隔離し、self
内で独自に定義したメソッド内で作成するようにする。- 依存が明らかになり、再利用の障壁を低くする。
-
||=
演算子(インスタンスの作成が引き延ばされる)
-
- 依存が明らかになり、再利用の障壁を低くする。
- 他のクラスのインスタンス変数の作成を
- 依存オブジェクトの注入
-
self
以外に送られるメッセージがある時- 外部的な依存を取り除き、専用のメソッド内にカプセル化する
- メソッドは外部のオブジェクトから独立し、
self
に送るメッセージに依存するようになる。
- メソッドは外部のオブジェクトから独立し、
- 外部的な依存を取り除き、専用のメソッド内にカプセル化する
-
メッセージが要求する引数がある時
- オプションハッシュを受け取る
- 引数の順番に対する依存が全て取り除かれる。
- ハッシュ内の「キー」名が、引数に関する明示的なドキュメントとなる。
- ハッシュは存在しないキーに対しては
nil
を返すため、明示的にデフォルト値を設定できる。-
||
演算子,fetch
メソッド,merge
メソッド
-
- オプションハッシュを受け取る
-
引数の順番が固定されている時 (依存せざるを得ないメソッドが外部のものである場合等)
- 外部のインターフェースを包み隠すためのメソッドを1つつくる。
-
Wrapper
モジュール( = 『ファクトリー』)を作成する。- 他のオブジェクトを作成することが目的のオブジェクト
- ファクトリーのメソッド内で外部のメソッドを呼ぶことで、固定順番の引数に対する複数の依存を回避することができる。
- ファクトリーはインスタンスが作られることを意図していない。
- ファクトリーは他のクラスにインクルードされることは想定されていない。
-
- 外部のインターフェースを包み隠すためのメソッドを1つつくる。
3-3. 依存方向の管理
-
依存関係を逆にする
-
依存方向の選択
- 「自身より変更されないものに依存しなさい」 = 以下概念のもとになる3つの事実
- あるクラスは、他のクラスよりも要件が変わりやすい
- 開発が活発なフレームワーク等
- 具象クラスは、抽象クラスよりも変わる可能性が高い
- 依存オブジェクトの注入等
- 抽象化されたものへの依存は、具象的なものへの依存よりも常に安全。
- 多くのところから依存されたクラスを変更すると、広範囲に影響が及ぶ
- 大量に依存されたクラスを避ける
- あるクラスは、他のクラスよりも要件が変わりやすい
- 「自身より変更されないものに依存しなさい」 = 以下概念のもとになる3つの事実
-
問題となる依存関係を見つける
- 具象クラスが多量の依存関係を持っていたら、心の警報を鳴らすべき。
-
依存方向を制御し、自分より変更の少ないクラスに依存するのがメンテナンスの鍵。
オブジェクト中心の設計からメッセージ中心の設計へ
4.柔軟なインターフェースをつくる
- 設計とは、オブジェクトが何を知っているか(オブジェクトの責任)や、誰を知っているか(オブジェクトの依存関係)だけではない。
- オブジェクトが互いに、どのように会話するか。
4-1. インターフェースを理解する
- パブリックインターフェース
- 外部に晒されたメソッド
- クラスはメソッドを実装し、そのうちいくつかは他のオブジェクトから使われることが意図されている。
- 外部に晒されたメソッド
4-2. インターフェースを定義する
-
パブリックインターフェースの特性
- クラスの主要な責任を明らかにする
- 外部から実行されることが想定される
- 気まぐれに変更されない
- 他者がそこに依存しても安全
- テストで完全に文書化されている
-
プライベートインターフェースの特性
- 実装の詳細に関わる
- 他のオブジェクトから送られてくることは想定されていない
- どんな理由でも変更され得る
- 他者がそこに依存するのは危険
- テストでは、言及されないこともある
-
パブリックインターフェースは、クラスの責任を明確に述べる契約書。
-
メソッドにパブリックやプライベートと印を付けることは、クラスの使用者に対し、どのメソッドには安全に依存できそうか、ということを伝えていることになる。
4-3. パブリックインターフェースを見つける
-
アプリケーションの設計
-
『ドメインオブジェクト』
- 「データ」と「振る舞い」の両方を兼ね備えた「名詞」を表す、永続するクラス。
- 大きくて目に見える現実世界のものを表し、かつ最終的にデータベースに表されるもの。
- ドメインオブジェクトは、アプリケーションを設計する上で、中心となるものではない。
-
オブジェクトではなく、オブジェクト間で交わされるメッセージに注意を向ける。
- ユースケースを満たすために必要な「オブジェクト」と「メッセージ」の両方について、まず見当をつけるべき。
-
-
シーケンス図を使う
- クラス間のメッセージ交換(パブリックインターフェース)をあらわにし、「この受け手は、このメッセージに応える責任を負うべきなのだろうか?」と問う。
- クラスと、クラスがだれと何を知るか => メッセージを決め、それをどこに送るか
- 「このクラスが必要なのは知っているけれど、これは何をすべきなんだろう」 => 「このメッセージを送る必要があるけれど、だれが応答すべきなんだろう」
-
送り手の「どのように」を伝えるのではなく、受け手に「何を」を頼む
- 「どのように」を知る責任を他のクラスに渡す
- パブリックインターフェースが小さいということは、他のところから依存されるメソッドがわずかしかないことを意味する
- コードの柔軟性とメンテナンス性の向上。
-
コンテキストの独立を模索する
- 最も良い状況は、オブジェクトがそのコンテキスト(他のクラスの持つメッセージに応答できるオブジェクトを持ち続けること)から完全に独立していること。
- 「何を」と「どのように」の違いに集中する
- 依存オブジェクトの注入
- 相手が誰かを知らずとも、他の誰かと共同作業するためのテクニック
- メッセージの受け手を信頼し、適切に振る舞ってくれることを期待する。
- 最も良い状況は、オブジェクトがそのコンテキスト(他のクラスの持つメッセージに応答できるオブジェクトを持ち続けること)から完全に独立していること。
-
信頼の重要性
- 「私は自分が何を望んでいるかを知っているし、あなたがそれをどのようにやるかも知っているよ」 => 「私は自分が何を望んでいるかを知っていて、あなたが何をするかも知っているよ」 => 「私は自分が何を望んでいるかを知っているし、あなたがあなたの担当部分をやってくれると信じているよ」
- この手放しの信頼が、オブジェクト指向設計の要
- 「私は自分が何を望んでいるかを知っているし、あなたがそれをどのようにやるかも知っているよ」 => 「私は自分が何を望んでいるかを知っていて、あなたが何をするかも知っているよ」 => 「私は自分が何を望んでいるかを知っているし、あなたがあなたの担当部分をやってくれると信じているよ」
-
新しいオブジェクトの必要性の発見
- 単一責任の原則が破られている時。
- オブジェクトが他のオブジェクトにどのように振る舞うかを伝えている上、膨大なコンテキストを求めている。
- 新しいオブジェクトは、そこにメッセージを送る必要性があったために発見される
- 単一責任の原則が破られている時。
4-4. 一番良い面(インターフェース)を表に出すコードを書く
-
インターフェースこそが、アプリケーションを定義し、未来を決定づける。
-
クラスをつくるときは、毎回インターフェースを宣言するようにする。
- 「パブリック」インターフェースに含まれるメソッド
- 明示的にパブリックインターフェースだと特定できる
- 「どのように」よりも、「何を」になっている
- 名前は、考えられる限り、変わり得ないものである
- オプション引数として、ハッシュを取る
- 「パブリック」インターフェースに含まれるメソッド
-
Rubyの
public
,protected
,private
キーワード- これらのキーワードを使うことによって、次の2つのことを伝えられる
- 「将来の」プログラマーが持つ情報よりも、今の自分の方がより良い情報を持っていると信じている
- 今の自分が不安定だと考えているメソッドを、将来のプログラマーに不用意に使われることは防がなければならないと信じている
- 何人ものかなり実力のあるRubyプログラマーが、あえてキーワードを省略する。
- キーワードを使ってメソッドのアクセスを制限できるなどという考えは、どのようなものであれ幻想にすぎない。
- これらのキーワードを使うことによって、次の2つのことを伝えられる
-
パブリックインターフェースを構築する際は、そのパブリックインターフェースが他者に要求するコンテキストが最小限になることを目指す。
- メッセージの送り手が、クラスがどのようにその振る舞いを実装しているかを知ることなく、求めているものを得られるように作る。
- 最初に書いた人がパブリックインターフェースを定義しなかったからとしても、自身の手でパブリックインターフェースを作る。
4-5. デメテルの法則
- オブジェクトを疎結合にするためのコーディング規則の集まり。
- パブリックインターフェースの正確な特定と定義ができていないことを意味する。
- 法則に違反していても、実害がない時もある。
- 「直接の隣人にのみ話しかけよう」
- 「ドットは1つしか使わないようにしよう」
- 中間のオブジェクトを介して遠くの属性を取得する。
- 明示的に中間のオブジェクトを特定し、必要に応じて変更する。
- 委譲を使う。
-
delegate
メソッド
-
- 「何を」求めているかから、メッセージチェーンを再考する
- 「どのように」を知る必要はない。
- メッセージに基づく視点に移行してメッセージを見つけたとすれば、そのメッセージは自ずと、何らかのオブジェクトのパブリックインターフェースとなる。
- そのオブジェクトが何のオブジェクトなのかは、メッセージ自体が導いてくれる。
- パブリックインターフェースの正確な特定と定義ができていないことを意味する。
5.ダックタイピングでコストを削減する
- 『ダックタイピング』とは
- いかなる特定のクラスとも結びつかないパブリックインターフェース
- さながらカメレオン。「もしオブジェクトがダック(アヒル)のように鳴き、ダックのように歩くならば、そのクラスが何であれ、それはダックである。」
5-1. ダックタイピングを理解する
-
アプリケーションによっては、複数のクラスをまたぐインターフェースをいくつも定義することもある。
- 重要なのは、オブジェクトが何であるかではなく、何をするか
-
オブジェクトのクラスについての不明瞭さを大目に見るという能力は、自信を持った設計者であることを証明する。
- オブジェクトを、そのクラスではなくあたかも振る舞いによって定義されているかのように扱えるようになる。
ポリモーフィズム
- 多岐にわたるオブジェクトが、同じくメッセージに応答できる能力を指す。
- 1つのメッセージが多くの(poly)形態(morphs)を持つ。
5-2. ダックを信頼するコードを書く
-
設計上で難しいこと
- ダックタイプが必要であることに気づくこと
- そのインターフェースを抽象化すること
-
隠れたダックを認識する方法
- クラスで分岐するcase文
- kind_of?とis_a?
- responds_to?
-
そのメソッドの引数が望むものは何だろうか、と問いかけてみる。
- その問いへの答えに、送るべきメッセージが示される。このメッセージから、根底にあるダックタイプが定義されはじめる。
-
他のオブジェクトを信頼するというよりも、制御しているような、不必要な依存は含まないようにする。
-
ダックを信頼する。
- ダックタイプをつかめたら、そのインターフェースを定義し、必要なところで実装し、それが正しく振る舞ってくれると信頼する。
-
ダックタイプを文書化する。
- ダックタイプを作るときは、そのパブリックインターフェースの文書かとテストを両方ともしなければならない。
-
ダック間でコードを共有する。
- 共通するメソッドを定義するとき、インターフェースのみを共有し、実装は共有しない。
- たびたび振る舞いもいくらか共有する必要がある。
-
賢くダックを選ぶ
- 根底にあるダックが、Rubyの基本的なクラスへの変更を要する場合、リスクとトレードオフが存在し、設計の目的がコストを下げることであることを理解する。
5-3. ダックタイピングへの恐れを克服する
-
静的型付けによるダックタイプの無効化
-
動的型付けと静的型付けの比較
- 静的型付け
- コンパイラがコンパイル時に型エラーを発見してくれる <=> コンパイラが型を検査しない限り、実行時の型エラーが起こる
- 可視化された型情報は、文書の役割も果たしてくれる <=> 型がなければプログラマーはコードを理解できない。プログラマーはオブジェクトのコンテキストからその型を推測することができない
- コンパイルされたコードは最適化され、高速に動作する <=> 一連の最適化がなければ、アプリケーションの動作は遅くなりすぎる
- 動的型付け
- コードは逐次実行され、動的に読み込まれる。そのため、コンパイル/makeのサイクルがない。 <=> アプリケーション全体の開発は、コンパイル/makeのサイクルがない方が安全
- ソースコードは明示的な型情報を含まない <=> 型宣言がコードに含まれない時の方がプログラマーにとって理解するのが簡単。そのコンテキストからオブジェクトの型は推測できる。
- メタプログラミングがより簡単 <=> メタプログラミングは、あることが望ましい言語機能
- 静的型付け
-
型エラーを防ぐのはプログラマー自身の知力。静的型付けによって安全になるという考えは幻想。
-
コードの良さは結局テストの良さ。
-
ダックタイピングは、動的型付けの上に成り立つ。
6.継承によって振る舞いを獲得する
6-1. クラスによる継承を理解する
- 継承とは、「メッセージの自動委譲」の仕組み
6-2. 継承を使うべき箇所を識別する
- ダックタイプを見つけたときのように、サブクラスを見つけることができる。
- 「自身の分類を保持する属性」を確認しているif文、type(型)、category(分類)など。
- bad: 「『あなたが誰なのか』知っている。なぜなら私は『あなたがすること』を知っているのだから」 => サブクラスの存在を示している。
- 「自身の分類を保持する属性」を確認しているif文、type(型)、category(分類)など。
- 「単一継承」。サブクラスは親となるスーパークラスを1つしか持つことができない。
- サブクラスはスーパークラスを「特化したもの」。
- サブクラスはスーパークラスのパブリックインターフェースを全て持っている。
6-3. 継承を不適切に適用する
- superを送ると、必要ではない、全く意味を成さない振る舞いを継承することになる。
6-4. 抽象を見つける
- 継承のルール
- モデル化しているオブジェクトが「汎化-特化の関係」を持っていること
- 正しいコーディングテクニックを使っていること
- 抽象的なスーパークラスを作る
- スーパークラスは、それ自体で完全なオブジェクトとはならない。
- スーパークラスにnewメッセージを送ることは到底考えられない。
- Javaの
abstract
キーワードのように、明示的にクラスを抽象概念として宣言する構文を持つオブジェクト指向プログラミング言語も存在する。- Rubyは他者を信頼する性質から、そのようなキーワードは備えていない。
- 抽象クラスはサブクラスが作られるために存在し、これが唯一の目的。
- サブクラス間で共有される振る舞いの共通の格納場所を提供する。
- サブクラスを1つしか持たない抽象的なスーパークラスを作ることは意味がない。
- 正しい抽象を特定するのが最も簡単なのは、存在する具象クラスが少なくとも3つある時。
- 抽象的な振る舞いを昇格する
- 新たな継承の階層構造へとリファクタリングする際は、抽象を昇格できるようにコードを構成するべきであり、具象を降格するような構成にはすべきではない。
- 具象から抽象を分ける。
- テンプレートメソッドパターンを使う。
- スーパークラス内で基本の構造を定義し、サブクラス固有の貢献を得るためにメッセージを送るというテクニック。
- スーパークラスはサブクラスに構造(共通のアルゴリズム)を提供する。
- 全てのオブジェクトにおいて、あるメソッドは同じ初期値が用いられ、またあるメソッドについては異なる初期値が用いられるようになる。
- テンプレートメソッドパターンを使うどのクラスも、その送信するメッセージの全てに必ず実装を用意するようにする。
- テンプレートメソッドの要件は、有用なエラーを発生させる、合致するメソッドを実装することで常に文書化する。
6-5. スーパークラスとサブクラス間の結合度を管理する
-
強固に結合されたクラス同士は互いに密着し、それぞれを独立に変更することは不可能になってしまう。
- サブクラスが
super
を送るとき、事実上スーパークラスのアルゴリズムを知っているという宣言であり、その知識に「依存」している。
- サブクラスが
-
フックメッセージを使って、サブクラスを疎結合にする。
-
super
を送るよう求めるのではなく、スーパークラスが代わりに「フック」メッセージを送るようにする。 - フックメソッドを使うことで、
super
の送信を強制せずとも継承者がスーパークラスに特化を提供できるようにする。
-
7.モジュールでロールの振る舞いを共有する
7-1. ロールを理解する
-
元々無関係だったオブジェクトが共通のロールを担うようになると、オブジェクトは互いに関係を持つようになる。ダックタイプはロール。
-
名前をつけてメソッドのグループを定義する方法を『モジュール』と呼ぶ。
- 様々なクラスのオブジェクトが、1か所に定義されたコードを使って共通のロールを担うための完璧な方法と言える。
-
オブジェクトが応答できるメッセージの集合には、次の4種類のメッセージが含まれる。
- 自身が実装するメッセージ
- 自身より上の階層の、全てのオブジェクトで実装されるメッセージ
- 自身に追加される、全てのモジュールで実装されるメッセージ
- 自身より上の階層のオブジェクトに追加される、全てのモジュールで実装されるメッセージ
-
オブジェクトは自身を管理すべき
- 自身の振る舞いは自身で持つべき。不必要な依存を追加しないようにする。
-
依存オブジェクトは、初期化時に注入することで隠しておく。
-
モジュールを作ることにより、他のオブジェクトはこのモジュールを利用して、コードを複製することなくロールを担える。
-
1つのクラスに複数のモジュールをインクルードする場合、最後にインクルードされたモジュールのメソッドが、メソッド探索パスの先頭に来る。
-
Rubyの
extend
キーワードを使うと、オブジェクト1つだけにモジュールのメソッドを追加することもできる。-
extend
はモジュールの振る舞いをオブジェクトに直接追加する。- クラスをモジュールで
extend
する- 「そのクラスに」クラスメソッドが追加される。
- クラスのインスタンスを
extend
する- 「そのインスタンスに」インスタンスメソッドが追加される。
- クラスをモジュールで
-
7-2. 継承可能なコードを書く
-
アンチパターン
- オブジェクトが
type
やcategory
という変数名を使い、どんなメッセージをselfに送るかを決めているパターン。- 共通のコードは抽象スーパークラスにおき、サブクラスを使って異なる型を作るべき。
- メッセージを受け取るオブジェクトのクラスを確認してから、どのメッセージを送るかをオブジェクトが決めているパターン
- 受け手のオブジェクトはダックタイプのインターフェースを実装するべき。
- ダックタイプは振る舞いまで共有することもあるため、その場合共通のコードはモジュールに置き、そのモジュールをそれぞれのクラスやオブジェクトにインクルードすることでロールを担わせる。
- 受け手のオブジェクトはダックタイプのインターフェースを実装するべき。
- オブジェクトが
-
抽象スーパークラス内のコードを使わないサブクラスがあってはならない。
- 抽象を正しくひとつに絞り込めなかったり、抽象化できる共通のコードが存在しなかったりするときは、継承を使っても設計の問題は解決できない。
-
サブクラスはスーパークラスと置換できることを約束する。
- 他のオブジェクトに自身の型を識別させ、自身の扱いや何が期待できるのかを決めさせることは、どんなことであっても許されない。
リスコフの置換原則
-
「システムが正常であるためには、派生型は上位型と置換可能でなければならない」
- Rubyらしく言い換えれば、「オブジェクトは自身が主張する通りに振る舞うべき」
-
テンプレートメソッドパターンを使う
- 抽象を継承する具象では、テンプレート化されたメソッドをオーバーライドすることで特化を行う。
- テンプレートメソッドを使うことで、何が変化するもので何が変化しないものなのかを明確に決めざるを得なくなる。
- 抽象を継承する具象では、テンプレート化されたメソッドをオーバーライドすることで特化を行う。
-
継承する側でsuperを呼び出すようなコードを書くのは避ける。
- 代わりにフックメッセージを使う。
- 抽象クラスのアルゴリズムを知っておく責任からは解放されながらも、アルゴリズムに加わることはできる。
- 代わりにフックメッセージを使う。
8.コンポジションでオブジェクトを組み合わせる
8-1. 自転車をパーツからコンポーズする
- 「Bicycle has-a Parts(全てのBicycleがPartsオブジェクトを必要とする)」 という関係を持つ = コンポジション
- 抽象Partsクラスを作成する。
8-2. Partsオブジェクトをコンポーズする
- Partを作る。Partsは複数のPartオブジェクトを持つ。
- 個々のPartオブジェクトを、Partsオブジェクトにひとまとめにしてグループ化する。
- Partオブジェクトの配列のそれぞれは、Partのロールを担うオブジェクト。
- Partクラスのインスタンスではない。
- Partクラスの種類でもない。
8-3. Partsを製造する
- 『ファクトリー』
- 他のオブジェクトを作るオブジェクト。
- RubyのOpenStructクラス
- いくつもの属性を1つのオブジェクトにまとめるための、便利な方法。
- Struct
- 初期化時に順番を指定して引数を渡す必要がある。
- OpenStruct
- 初期化時にハッシュをとり、そこから属性を引き出す。
- Struct
- いくつもの属性を1つのオブジェクトにまとめるための、便利な方法。
8-4. コンポーズされたBicycle
- Partsの役割はPartsが果たす
- Partの役割はOpenStructが担い、それはname, description, needs_spareを実装する。
集約 - 特殊なコンポジション
- コンポジションとは?
- 2つのオブジェクトが「has-a」関係を持つ
- 包含される側のオブジェクトが包含する側のオブジェクトから独立して存在し得ないもの
- 例:料理は前菜を持つが、一度料理が食べられてしまえば、前菜も同様になくなってしまう。
- 集約とは?
- 包含される側のオブジェクトの存在が独立している。
- 例:大学が消えて学部も消えたとして、それでもその教授たちが存在し続ける。
- 包含される側のオブジェクトの存在が独立している。
8-5. コンポジションと継承の選択
-
クラスによる継承
- オブジェクトを階層構造に構成するコストを払う代わりに、メッセージの異常にコストはかからない。
-
コンポジション
- オブジェクトは構造的に独立して存在できるようになるが、明示的なメッセージ委譲のコストを払う必要がある。
-
コンポジションが持つ依存は、継承が持つ依存よりもはるかに少ないので、コンポジションを優先する。
-
継承の利点
- TRUEである。
- オープン・クローズド(拡張には開いており、修正には閉じている)
-
継承のコスト
- 誤って継承を選択した場合、コードを複製するか、再構成せざるを得ない。
- 他のプログラマーによって、全く予期していなかった目的のために使われるかもしれない。
-
コンポジションの利点
- 構造的に独立しているため、利用性が高く、想定していなかった新たなコンテキストでも簡単に利用できる。
- パーツから成るオブジェクトの、組み立てのルールを規定するにはとても優れている。
-
コンポジションのコスト
- 多くのパーツに依存する。
- 個々の部品は小さく、簡単に理解できるものであったとしても、組み合わせられた全体の動作は、理解しやすいとは言えない。
- メッセージの自動的な委譲を犠牲にする。
- 明示的にどのメッセージを誰に委譲するかを必ず知っていなければならない。
- ほぼ同一なパーツが集まっているコードを構成する問題に対しては、そこまでの助けにならない。
- 多くのパーツに依存する。
-
is-a関係に継承を使う
- 継承とは、特殊化です。
- 継承が最も適しているのは、過去のコードの大部分を使いつつ、新たなコードの追加が比較的少量の時に、既存のクラスに機能を追加する場合です。
-
behaves-like-a関係にダックタイプを使う
- あるオブジェクトが何かロールを担っているにもかかわらず、そのロールがそのオブジェクトの主な責任ではないとき。
- 互いに関係しないオブジェクトが、同じロールを担いたいという欲求を共有する時。
- 共通の振る舞いはRubyのモジュールに定義し、コードを複製せずともオブジェクトがロールを担えるようにする。
-
has-a関係にコンポジションを使う
- 多くのオブジェクトが、いくつものパーツを含んでおり、それらのオブジェクトの総和はそれらのパーツの総和を上回る時。
- オブジェクトがパーツを持てば持つほど、コンポジションでモデル化されるべきであるという可能性が高まる。
-
コンポジション、クラスによる継承、モジュールを使った振る舞いの共有といったテクニックを適切に使えるようになるための学習は、経験と判断の問題。
テストの設計
9.費用対効果の高いテストを設計する
- 変更可能なコードの実践には3つのスキルが欠かせない。
- オブジェクト指向設計の理解。
- コードのリファクタリングに長けていること。
- リファクタリングとは、ソフトウェアの外部の振る舞いを保ったままで、内部の構造を改善していく作業を指す。 - 価値の高いテストを書く能力。
9-1. 意図を持ったテスト
-
テストの意図
- バグを見つける
- 仕様書となる
- 設計の決定を遅らせる
- テストはそのインターフェースが正しく振る舞い続けることを証明するので、根底にあるコードを変更しても、テストの書き直しが求められることはない。
- 抽象を支える
- 設計の欠陥を明らかにする
-
より少ないテストを書く
- 最も安定している、パブリックインターフェースに定義されるメッセージを対象としたものを書くべき。
9-2. 受信メッセージをテストする
-
依存されていない受信メッセージのテストは削除する。
-
テストダブルを作る。
- ロールを担うフェイクオブジェクト。ロールの担い手を様式化したインスタンス。テストのみで使われるもの。
- ダブルはメソッドを「スタブ」する。つまり、予め詰められた答えを返すメソッドを実装する。
9-3. プライベートメソッドをテストする
-
プライベートメソッドは冗長化つ不安定(変わりやすい)で、メンテナンスの時間が増えてしまうので無視する。
-
そもそもプライベートメソッド自体を作らないようにする。
-
プライベートメソッドを大量に持つオブジェクトからは、責任を大量に持ちすぎた設計の臭いが漂ってしまう。
- それらのオブジェクトを新しいオブジェクトに切り出すことを考える。
-
「プライベートメソッドは決して書かないこと。書くとすれば、絶対にそれらのテストをしないこと。ただし、当然のことながら、そうすることに意味がある場合を除く。」
-
9-4. 送信メッセージをテストする
-
送信メッセージは「クエリ(質問)」か「コマンド(命令)」のどちらか。
- クエリメッセージは、それらを送るオブジェクトにのみ問題となる。
- コマンドメッセージは、アプリケーション内の他のオブジェクトから見える影響を及ぼす。
-
テストではselfに送られたメッセージは無視されるべき。
- 外に出ていくクエリメッセージもまた、無視されるべき。
-
前もって依存オブジェクトを注入していれば、かんたんにモックに置き換えられる。
- モックに期待を設定することによって、テスト対象オブジェクトがその責任を果たすことを、他のどこかに属する表明を複製することなく証明できる。
9-5. ダックタイプをテストする
-
アンチパターンを使いつつもテストがないコードに遭遇した場合、テストを書く前にリファクタリングをして、より良い設計にすることを考えましょう。
-
ロールのテストは一度だけ書き、全ての担い手間で共有されるようにすべき。
- Minitestはテストの共有をRubyのモジュールでサポートしている。
- 振る舞いをモジュールに切り出す。
-
テストダブルをロールの他の担い手と同じように扱い、テストでその正しさを証明するのであれば、テストの壊れやすさは回避でき、影響を気にすることなくスタブができる。
-
ダックタイプのテストをしたいという要望によって、ロールに対する共有可能なテストの必要性が生まれた。そして、一度このロールに基づいた視点を獲得してしまえば、様々な状況でそれを活用できる。テスト対象オブジェクトから見れば、他のオブジェクトは全てロール。そして、オブジェクトをそのロールの表現であるように扱うことで結合はゆるくなり、柔軟性は高まる。これは、アプリケーションとテストの両方で言える。
9-6. 継承されたコードをテストする
-
リスコフの置換原則
- 派生型はその上位型と置換可能であるべき。
-
リスコフの置換原則にしたがっていることを証明する最も簡単な方法は、その共通の契約に共有される(インターフェースの)テストを書き、全てのオブジェクトのテストを(具象クラスのテストに)インクルードすること。
-
サブクラスに共通する振る舞いのテストを書く。
-
抽象クラスのインターフェースのテストと、サブクラスに共通する振る舞いのテストにより、サブクラスが標準から外れていないことを確信できる上、新参のメンバーは全く安全にサブクラスを作れるようになる。新たに加わるプログラマーは、用件を掘り起こすためにスーパークラスを探し回る必要はない。新たにサブクラスを描くときは、単にこれらのテストをインクルードすれば良い。
-
抽象クラスのインスタンスの作成は、難しいか、かなり難しいか、不可能かのどれか。
-
サブクラスに特化した部位をテストする際に重要なのは、スーパークラスの知識をテスト内に埋め込まないこと。
-
スーパークラスが具象的な特化を獲得するためにテンプレートメソッドを使っていれば、通常はサブクラスによって提供される振る舞いをスタブできる。
- スタブを用意するためにサブクラスを作るというアイデアは、多くの状況で役に立ち、リスコフの置換原則を破らない限り、どのテストでもこのテクニックを使える。
-
注意深く書かれた継承構造のテストは簡単。共有可能なテストを一つ、全体のインターフェースに対して書き、もう一つをサブクラスの責任に対して書く。
- 1つ1つ責任を隔離していく。
- サブクラスの特化をテストするときは、スーパークラスの知識がサブクラスのテストに漏れてこないように注意を払う。
- 抽象スーパークラスのテストは難しい。
- リスコフの置換原則を活用し、テスト専用のサブクラスを作るときには、それらのサブクラスにもサブクラスの責任のテストを適用するようにする。
-
最も良いテストとは、対象のコードと疎結合であり、全てに対し一度だけテストをし、そしてそれが適切な場所で行われているもの。