オブジェクト指向設計実践ガイド
https://scrapbox.io/files/6365028a98c420001d3e9ad7.png https://amzn.to/3Tdrpgm
はじがき
ソフトウェアの要件は必ず変わる
コードの運用期間全体に渡ってのメンテナンス性が現状の最適化よりも優先される
はじめに
品質の高いコードを書くことは楽しい(一石二鳥)
純粋にコードを書くことを楽しめる
楽しい時は自身が有能、楽しくないなら無能ということになる
質の悪いソフトウェアはプログラマーの目的を阻害する(不幸になる・生産性は損なわれる)
達成した総量とそこに費やされた努力の総量を絶えず比較
仕事のコストが達成した目的の価値を上回れば努力が無駄になったように感じる
第1章 オブジェクト指向設計
手続き的: 時の流れの中で出来事が順番に起こり、過ぎ去っていく
オブジェクト指向: 様々なオブジェクトが相互作用する(オブジェクト間の「自発的な」相互作用)
世界をあらかじめ決められた手続きの集まりとは考えない
オブジェクト指向は振る舞いの新しい組み合わせが自然に現れる世界。
spouse_steps_on_catの手続きコードをわざわざ書く必要はない。必要なものは歩き回る配偶者オブジェクトと、踏みつけられることを嫌う猫オブジェクトだけ。
この2つのオブジェクトを同じ部屋に配置すれば予期しない組み合わせの振る舞いはいずれ起こる。
オブジェクト指向設計が失敗する原因:
コーディングテクニックの問題というよりは、視点の置き方に失敗している
1.1 設計の賛美
ソフトウェア = 応用ソフト
目的があるから作られる
楽しさと生産性を秤に掛ける必要はない
最も効率よくソフトウェアを生産するコーディングは楽しいため
最も効率よくソフトウェアを生産するコーディングとは具体的には正しいオブジェクト指向設計の手法であり、それに従うことで費用対効果が高いソフトウェアを作れる
設計が解決する問題
一度書いたらもう永久に変化しないアプリケーションでは設計は不要
現実的には変わらないアプリケーションは存在しない
要件の変更はプログラミングにおける摩擦力と重力
変更が困難な理由
オブジェクト指向のアプリケーションは部品から構成される
ここでの部品とはオブジェクトのことである
オブジェクトの相互作用(オブジェクトが他のオブジェクトにメッセージを送信すること)で全体(システム)の振る舞いが生まれる
正しいメッセージを正しいオブジェクトに届けるためには、メッセージの送り手が受け手のことを知っている必要がある。
-> この知識が2つのオブジェクト間に依存関係を作り出す。この依存関係が変更の邪魔をする
オブジェクト指向設計とは「依存関係を管理すること」
-> オブジェクトが変更を許容できるようなかたちで依存関係を構成するためのコーディングテクニック
設計されていないと管理されていない依存関係が大混乱を引き起こす
オブジェクトが互いを知りすぎる
↓
オブジェクトを1つ変更すると一緒に動くオブジェクトにも変更を加えることになる
↓
すると今度はそのオブジェクトと一緒に・・
↓
Aを変更したらB,C..Zまで壊れる羽目に
オブジェクトが知識を持ちすぎている
そのようなオブジェクトは自分のいる世界に対して多くのことを望む
このオブジェクトは気難しく、物事が変わらず「ただそのように」あるように求める
これがそのままオブジェクト自身の制約に
= 再利用できない
アプリケーションが小さければ設計が貧弱でも耐えきれる
設計が貧弱で大きなアプリケーションに育つと目も当てられない状況に・・
単純であるべき変更がアプリケーション内部を次々と伝わり、あらゆるところでコードを破壊する
設計の実用的な定義
アプリケーションはコードの集まり = 設計とはコードの構成のこと
設計が難しい理由
すべての問題が2つの要素を抱えている
今ユーザに届けようと計画している機能のコードをただ書くだけではいけない
その後の変更も受け入れられるものでなければいけない
設計に求められるのは一種の合成(以下の組み合わせで優れた構成を考える)
アプリケーションに求められる機能全体の知識 + それぞれの設計案にかかるコストと利点についての知識
設計において未来を考慮するという考えは、まだ知られていない要件を想定して今のうちに実装をしておくことではない
未来を推測するのではなく、未来を受け入れるための選択肢を保護するためのもの
設計の目的: 「あとにでも」設計をできるようにすることであり、第一の目標は変更コストの削減
1.2 設計の道具
設計とは決められた一連のルールに従う行為ではなく、枝分かれする道を進む旅
オブジェクト指向の設計者が持つべき道具は「原則とパターン」
設計原則
SOLID: オブジェクト指向設計で最もよく知られる5つの原則を表した頭文字
code:_
Single Responsibility(単一責任)
Open-Closed(オープン・クローズド)
Liskov Substitution(リスコフの置換)
Interface Segregation(インターフェース分離)
Dependency Inversion(依存性逆転)
DRYやデメテルの法則も有名
これらの設計原則の有効性は研究で検証済み(NASAなどが検証者)
デザインパターン
GoFのデザインパターンが有名
デザインパターンを知っているもの同士ならコミュニケーションが円滑に
初心者はパターンの乱用をしてしまうことが多い
1.3 設計の行為
設計が失敗する原因
設計が十分じゃないと失敗する。
ただし、設計技法を全く知らなかったり設計者として未熟だったりしてもアプリケーションは作れてしまう。
Q. 変更を要求しますが対応できますか?
設計を全く理解していない開発者「はい、その機能は追加できますがすべてが壊れます」
設計は知っているものの正しく適用ができない開発者「いいえ、その機能は追加できません。それをやるための設計はしていません」
最終的に、設計の行為と実装の行為が乖離した時にオブジェクト指向ソフトウェアは失敗する
設計とは漸進的な発見のプロセスであり、繰り返しのフィードバックを頼りに進んでいく
そのフレームがアジャイル開発
アジャイル開発の反復的な性質によって設計は定期的に調整され、自然に進化する。
設計が早すぎる、つまり必要な調整がわかるにはまだまだ早い時点で設計が行われると、コードには初期の間違った理解が固く埋め込まれてしまう。
設計の専門家「ええと、もちろんこれを書くことはできます。けれどこれはあなたが本当に求めているものではないですし、いずれ後悔しますよ」
設計をいつ行うか
アジャイルで正しいとされている考え
顧客にはすぐにソフトウェア(の試作品)を見せる
ソフトウェアは小さな繰り返しで作っていく
開発を繰り返すことで徐々に顧客が本当に必要とするものを満たすアプリケーションに近づけていくべき
= 顧客と共同作業をするのが最も費用対効果が高い方法
アジャイルが正しいなら以下は真である
BDUF(Big Design Up Front)を作ることに意味は全くない
アプリケーションの完成時期は誰にも予測できない
アジャイルが嫌いな人は上記を認めたくない人
設計書は責任の押し付け合いの道具にされてしまう
アジャイルが「BDUFを作るな」と主張するのは設計を全くするなという意味ではない
BDUFとオブジェクト指向設計とでは「設計」の意味が異なる
BDUF = 提案されたアプリケーションのすべての機能の、想定される未来の内部動作をすべて特定して完全に文章化すること(※不可能)
オブジェクト指向設計はもっと小さな領域に関心を向ける
変更が簡単になるようにどんなコード構成にするかどうか
設計を判断する
オブジェクト指向設計のメトリクスの測定結果が悪い = 設計が悪い
測定結果がよかった ≠ 設計がうまくいっている(過度な設計は不適切だがスコアは高くなる)
オブジェクト指向設計のメトリクスは正しい方法で間違ったことをしている設計を見分けることはできない
= オブジェクト指向設計のメトリクスの結果は鵜呑みにしない
今すぐに機能を手に入れることが何よりも優先すべきである状況(将来的にコストがどれだけ増えるかよりもスピード重視)もある
そういう状況で設計を妥協することは未来から時間を借りていると同じことで「技術的負債」と言われる(そして技術的負債には往々にして利息がつく)
設計者の目標は機能あたりのコストが最も低い方法でソフトウェアを書くこと
どの程度まで設計するか決断する際の要素 = 自身のスキルと結果が出るまでの時間
設計に半年かかり利益を生み出すようになるまで1年かかるなら価値はないが、設計に午前の半分しかかからずその日から成果が出るならアプリケーションを運用する限り福利を享受できる
設計の損益分岐点はプログラマーの熟練度によって左右される
下手の考え休むに似たり
1.4 オブジェクト指向プログラミングのかんたんな導入
オブジェクト指向のアプリケーション: オブジェクトとオブジェクトの間で交わされるメッセージで構成される
クラスよりもメッセージの方がより重要
手続き型言語
手続き型言語 = 非オブジェクト指向
振る舞いとデータ間に隔たりがある
データと振る舞いは完全に別物
データは変数に納められ、振る舞いから振る舞いへと引き渡される
= データに対してなんでもできる
データはあたかも毎朝学校に送り出される子供のようであり、振る舞いはさながら送り出す保護者のようである
一旦送り出してしまえばあとは実際に何が起こるかは知るすべがない
オブジェクト指向言語
データと振る舞いを2つの独立した決して出会うことのない領域に分けたりはしない
その2つをオブジェクトにまとめる
データへのアクセスをコントロールするのはそのオブジェクトのみ
オブジェクトは互いにメッセージを送り合うことで互いの振る舞いを実行する
オブジェクトはどれもどれだけ多く、あるいは少なくデータを晒すかを自分で決めている
雪の結晶のようにこの世に2つとないオブジェクトが必要な時もある
ただ、振る舞いは同じだけどデータは異なるオブジェクトを大量に製造したいと思うことの方がずっと一般的
クラスは似たようなオブジェクトの構造の設計図
Rubyにデータ型はない -> 全てがオブジェクト
StringクラスもClassクラスのインスタンス
次第にオブジェクト指向アプリケーションはプログラマの扱う領域に特別に誂えた独自のプログラミング言語になっていく
-> 自身の領域に特化した言語が最終的に楽しみをもたらすのか苦痛をもたらすかは設計の問題
第2章 単一責任のクラスを設計する
オブジェクト指向のシステムの基礎は「メッセージ」
しかし、組織の構造で最も目立つのは「クラス」
メッセージこそ設計の核だが、このセクションでは明確で理解しやすいクラスに焦点を当てる
クラスはどのようなものにすればいいか
いくつ用意すればいいか
どんな振る舞いを実装するか
他のクラスについてどれぐらい知っているべきか
自身に関してどれぐらい曝け出せばいいか
これらに答えるのは容易ではない。
現段階で第一にやるべきことは「シンプルであれと主張すること」
目標はアプリケーションをモデル化すること
クラスを使い、「いますぐに」求められる動作を行い
かつ「あとにも」簡単に変更できるようにモデル化をする
この2つの基準はかなり異なる(それが設計の難しさ)
2.1 クラスに属するものを決める
メソッドをグループに分けクラスにまとめる
オブジェクト指向ではメソッドはクラス内に定義される
クラスはソフトウェアにおける仮想の世界を定義する
この仮想世界が以降の工程に関わる全員の想像力に制約を課す
= クラスを作ることは枠組みを作ることであり、この枠組みに縛られずに考えることは難しい
メソッドを正しくグループ分けし、クラスにまとめることはとても重要。
にも関わらずプロジェクトの初期段階では正しくグループ分けすることは到底できない。
なぜか? -> 初期段階での知識量はプロジェクト全体で言えば一番少ないから
= あとで変更できるように作ることが大切
変更がかんたんなようにコードを組成する
具体的な「かんたん」に対する定義
code:_
変更は副作用をもたらさない
要件の変更が小さければ、コードの変更も相応して小さい
既存のコードは簡単に再利用できる
最もかんたんな変更方法はコードの追加である。ただし追加するコードはそれ自体が変更が容易なものとする
上記に基づいてコードには次の性質が伴うべき
code:_
見通しがいい(Transparent): 変更するコードにおいても、そのコードに依存する別の場所のコードにおいても、変更がもたらす影響が明白である
合理的 (Reasonable) : どんな変更であっても、かかるコストは変更がもたらす利益にふさわしい
利用性が高い(Usable) : 新しい環境、予期していなかった環境でも再利用できる
模範的 (Exemplary) : コードに変更を加える人が、上記の品質を自然に保つようなコードになっている
見通しがよく、合理的で、利用性が高く、模範的(それぞれの頭文字をとってTRUE)
TRUEなコードを書くための最初の一歩はそれぞれのクラスが明確に定義された単一の責任を持つように徹底すること
2.2 単一の責任を持つクラスを作る
クラスはできる限り最小で有用なことをするべき = 単一の責任を持つべき
アプリケーション例: 自転車とギア
※この章は自転車の知識がないと理解できないので注意。以下理解が必要な用語を説明する
https://gyazo.com/7050db1deae47b52eff6af5092c641a3
チェーンリングがペダルの軸になる歯車で、前ギアとも言われたりもする
コグは後輪についている歯車で、後ギアとも言われたりする
※※※
※ ギア比
サイクリストはギアを「大きい」「小さい」ではなく歯数で比較する
ギア比 = チェーンリングの歯数 / コグの歯数
単体の歯車はスプロケット、歯車の噛み合わせはギアでそれぞれ表現されることが多い
e.g.
52x11の組み合わせならギア比は4.73
→ ペダルを一度漕ぐと車輪がほぼ5周する
※※※
※※※
※ ギアインチ
ギア構成が全く同じでも車輪のサイズが異なる場合の考慮も必要
-> ギアインチで比較
ギアインチ = 車輪の直径 * ギア比
ただし、
車輪の直径 = リム(タイヤがはまってる輪っかの外側の部分。ホイールの枠)の直径 + タイヤの厚みの2倍とする
※※※
ギア比を計算するRubyアプリケーションを書くとすると・・
ドメイン内のオブジェクトを表す名詞を見つけながら進むと「自転車」や「ギア」といった単語が目につく -> クラスの候補
※クラスになるにはデータと振る舞いを持っていなければいけないので、なんでも無条件にクラスにできるわけではなく、あくまで候補である
e.g.
「ギア」にはチェーンリング(サドルの下のギア)とコグ(後輪側のギア)、そして比があるのでデータと振る舞いがあると言える -> クラスになるのにふさわしい
メソッドに必要な引数の数の変更はそのメソッドを呼び出している箇所すべてを壊す
なぜ単一責任が重要なのか
変更がかんたんなアプリケーションは再利用がかんたんなクラスから構成される
再利用がかんたんなクラスとは、着脱可能なユニット
明確に定義された振る舞いから成り、周りとの絡み合いはわずかしかない
変更がかんたんなアプリケーションは積み木が詰まった箱のようなもの
必要な部品だけを選んで予想外のかたちに組み立てることができる
2つ以上の責任を持つクラスはかんたんに再利用することができない
多岐にわたる責任はクラス内部に完全に絡みついてしまいがち
結果、再利用したいときに必要な部分だけを手に入れることは到底不可能
このことからコードが複製されることがあるが、これは酷い案
多くのことをやりすぎているクラスへ依存する:
変更が加わるたびにそのクラスに依存するすべてのクラスを破壊する可能性がある
クラスが単一責任かどうかを見極める
クラスが別のどこかに属する振る舞いを含んでいるかどうかを見分ける方法は2つ
1. それに知覚があるかのように問いただす
e.g. Gearクラスの場合
「Gearさん、あなたの比を教えてれませんか?」 <- 理にかなっている
「Gearさん、あなたのgear_inchesを教えてくれませんか?」 <- しっくりこない
「Gearさん、あなたのタイヤ(のサイズ)を教えてくれませんか?」 <- 完全に馬鹿げている
2. 1文でクラスを説明してみる考えつく限り短い説明に「それと」が含まれていれば、おそらくクラスは2つ以上の責任を負っている
「または」が含まれるようであればクラスの責任は2つ以上あるだけではなく、互いにあまり関連もしない責任を負っている
この概念は凝集度によって表される
クラス内のすべてがそのクラスの中心的な目的に関連してればそのクラスの凝集度は高い(もしくは単一責任である)
設計を決定するときを見極める
未来にどんな機能の要求がくるかを知っていれば設計について完璧な判断を下せるが、そんなことは不可能
(情報が不十分で)早い段階で設計を決定してはいけない
「いますぐ改善」「あとで改善」間の緊張は常に存在する
設計を実施するかどうかの判断のために「今日何もしないことの、将来的なコストはどれだけだろう?」と自問する
-> 何もしないことによる将来のコストが今と変わらないときは決定は延期する
自分以外の他の開発者は設計の意図はコードに反映されていると信じているもの
コードが嘘をついているときには、その嘘を信じ、広めてしまうプログラマーの存在に注意
2.3 変更を歓迎するコードを書く
データではなく振る舞いに依存する
振る舞いはメソッド内に捉えられている
メッセージを送ることによって実行できる
単一クラスを作ればどんな些細な振る舞いもそれぞれがただ一ヶ所のみに存在するようになる(DRY)
振る舞いにどんな変更があっても1ヶ所コードを修正するだけで済む
オブジェクトは振る舞い以外にデータも持つ
インスタンス変数はアクセスメソッドで包み、直接参照しない
どんな変数も単なるオブジェクトであるかのように取り扱いができる
振る舞いのないデータとみなしたほうがいい時とそうでない時があり、大抵は後者なので部品はごく普通のオブジェクトだと区別してもらえたほうがいい
自身からデータを隠すべき
隠蔽することで予期せぬ変更がコードに影響を与えることを防ぐ
= 変数にアクセスするときはたとえそれをデータだと思っていたとしても、メッセージを送るようにする
データ構造の隠蔽
×: Arrayを使って複数のデータを渡し、どの添字にどのデータがあるのか知識を持たせる
○: オブジェクトのアクセサメソッドにメッセージを送る
後者で外部のデータ構造に強いコードができる
あらゆる箇所を単一責任にする
単一責任の概念自体はクラス以外のコードの多くの箇所で役立てられる
メソッドから余計な責任を抽出する
メソッドはクラスのように単一の責任を持つべき
メソッドに対しても(クラスと同様に)役割がなんであるかを質問し、1文で責任を説明できるようにする
設計がすでに明確ではないからこそ、リファクタリングが必要
行き先を知らずとも行き先に行くために優れた手法(good practices)が自ずと設計を明らかにする
単一責任メソッドがもたらす恩恵
隠蔽されていた性質を明らかにする: クラスが行うこと全体がより明確になる
コメントをする必要がない: メソッド名がそのままコメントの目的を果たす(自己文書化コード)
再利用を促進する: 他のプログラマーがコードの複製ではなく再利用をするようにする
他のクラスへの移動がかんたん: いくつものリファクタリングやメソッドの抽出をしなくても振る舞いの再構成が可能
責任が混沌としているクラスがあればそれらの責任は別のクラスに分ける
まだ取り除けない責任を見つけたら隔離する(※サンプルコードのGearとWheel参照)
本質的ではない責任がクラスにじわじわ入り込んでくるのを許してはいけない
2.4 ついに、実際のWheelの完成
新機能の要望によって得られる情報 = 設計の次を決定するために必要なもの (設計者が待ち望んでいるもの)
第3章 依存関係を管理する
オブジェクトの相互作用をなくすことはできない
1つのオブジェクトがすべてのことを知ることはできない -> 知らないことは他のオブジェクトに聞くしかない
オブジェクト指向アプリケーションで交わされるメッセージの量は膨大だが、その中にもパターンがある
オブジェクトに望まれる振る舞いは以下のいずれか
オブジェクト自身が知っている、または継承している
もしくはそのメッセージを理解する他のオブジェクトを知っている
単一の責任を持つオブジェクトは本質的に複雑な問題を解くためには他のオブジェクトとの共同作業をする必要がある
3.1 依存関係を理解する
一方のオブジェクトに変更を加えたとき、他方のオブジェクトも変更せざるを得ないなら片方に依存しているオブジェクトがある
依存関係を認識する
オブジェクトが次のものを知っているときはオブジェクトには依存関係がある
他のクラスの名前
self以外のどこかに送ろうとするメッセージの名前
メッセージが要求する引数
それらの引数の順番
先に述べたようにオブジェクト間に依存関係が築かれること自体は避けられない
ただし、不必要な依存は「合理性」を損なう
オブジェクト間の結合(CBO: Coupling Between Objects)
結合が強固になればなるほど、2つのオブジェクトが1つのユニットのように振る舞ってしまう
そのうち1つから再利用をすることが不可能になる
他の依存関係
破壊的な類の依存:
「「何かを知るオブジェクト」を知るオブジェクト」を知るオブジェクトがある <- いくつものメッセージをチェーンのように繋いで遠くのオブジェクトに存在する振る舞いを実行しようとしている場合。
「self以外のどこかに送ろうとするメッセージの名前」を知っているタイプの依存。
コードに対するテストの依存関係:
テストはコードを参照するが故に、コードに依存する -> コードと過度に結合したテストが書かれてしまう(テストがもたらす価値とそのコストが見合わない)
3.2 疎結合なコードを書く
依存はすべて接着剤のような小さな粒
多量の接着剤を使えばアプリケーションは1つのブロックに固められてしまう
依存を減らすこと = 必要のない依存を認識し、取り除くこと
依存オブジェクトの注入
: ダックタイプのオブジェクトを用いて、「他のクラスの名前」を知っているタイプの依存関係が発生しないようにするテクニックのこと
外部のクラス名に対する依存をどのように管理するかはアプリケーションに多大な影響を及ぼす
依存するものを常に気に留め、それらを注入することを習慣化させていればクラスは自然と疎結合になる
依存を隔離する
不必要な依存をすべて取り除くというのは理想論であり、現実的には不可能
できることはせいぜい現状の改善
不必要な依存を除去できないのならクラス内で隔離すべき
¥ 依存というのはすべて、クラス(の設計)を蝕もうとする外来のバクテリアのようなもの
依存は外からの侵入者であり、コードの脆さを表わしている
脆い外部メッセージを隔離する
外部メッセージ = self以外のどこかに送ろうとするメッセージ
引数の順番への依存を取り除く
多くのメソッドのシグネチャは引数を特定の固定された順番で渡してやる必要がある <- 結合の一種(メッセージが要求する引数とその順番を知っている)
= 初期化の際は引数にハッシュを使ってこれを避ける
外部フレームワークを用いる際にこの依存を強制されるのであれば、ファクトリーで依存を覆い隠す
ファクトリー: 他のオブジェクトを作成することが目的にオブジェクト
3.3 依存方向の管理
依存関係の逆転
依存関係を逆転させても害をないコードを簡単に書けることがある(本書におけるGear - Wheel)
依存性逆転の原則:
1. 上位のモジュールは、下位のモジュールに依存してはならない。
どちらのモジュールも「抽象」に依存すべきである。
2. 「抽象」は実装の詳細に依存してはならない。
実装の詳細が「抽象」に依存すべきである。
依存方向の選択
コードに関する事実
あるクラスは他のクラスよりも要件が変わりやすい
具象クラスは抽象クラスよりも変わる可能性が高い
多くのところから依存されたクラスを変更すると、広範囲に影響が及ぶ
¥ 自身より変更されないものに依存しなさい
変更の起きやすさを理解する
あるクラスは他のクラスよりも要件が変わりやすい
これは自身で書いてないコードにも当てはまる
Rubyの基本的なクラスが大きく変わる可能性は低い
フレームワークは枯れているかどうかで確率変動
黎明期のものは自身のコードよりも変わりやすい可能性も・・・
= アプリケーションのクラスは変更の起きやすさによって順位付けできる <- 依存の方向を決める際の一つのカギになる
具象と抽象を認識する
抽象: いかなる特定の実例(インスタンス)からも離れていること
抽象化されたものはそれらが共通し、安定した性質を表し、抽出元となった具象クラスよりも変わりにくい
抽象化されたものへの依存は具象的なものへの依存よりも常に安全
Rubyではインターフェースを定義するために明示的に抽象を宣言する必要はない
しかし、仮想的なインターフェースはクラス同様に現実的には存在している
このコンテキストでの「クラス」という用語は、「クラス」とこの種の「インタフェース」の両方を意味する
問題となる依存関係を見つける
Y軸に依存されている数、X軸に変わりやすさの表で考える
A. 抽象領域: 変更は起こりにくいが、依存されている数は多い = 変更されたら影響範囲が大きい
B,C.中立領域: 変更は起こりにくく、依存されている数も少ない(もしくはその逆) = 副作用はわずか
D. 危険領域: 変更は起こる、依存されている数は多い = 簡単な要求が悪夢のようなコーディングに化ける
Aは大抵抽象クラスかインターフェース
よく設計されたアプリケーションには必ずここに分類されるクラスがある
¥ 自身より変更されないものに依存しなさい
上記金言を遵守し、Dを作らない
第4章 柔軟なインターフェースを作る
クラスは静的構造でオブジェクト間での会話(メッセージのやりとり)は動的構造
オブジェクトが互いにどのように会話するかも、設計で考慮することに含まれる
オブジェクト間の会話は「インターフェース」を介して行われる
4.1 インターフェースを理解する
クラスが何を「するか」ではなく何を「明らかにするか」
全てを明らかにするクラスは、メソッドの特性や粒度を全く考慮しない
4.2 インターフェースを定義する
e.g. レストランの厨房
厨房では多くのことが行われるが、お客さんにその全てを公開していない
厨房にはお客さんが使うことだけが期待される「パブリック(公開された)」インターフェースがある -> メニュー(パブリックメソッド)
厨房内でやり取りされるメッセージ -> プライベートメソッド
メニューを使うことによって、厨房が「どのように」料理を作るかは一切関知せずに、「何を」望むかをお客さんに頼ませることができる
パブリックインターフェース
クラスの主要な責任を明らかにする
外部から実行されることが想定される
気まぐれに変更されない
他者がそこに依存しても安全
テストで完全に文書化されている
プライベートインタフェース
実装の詳細に関わる
他のオブジェクトから送られてくることは想定されていない
どんな理由でも変更され得る
他者がそこに依存するのは危険
テストでは、言及さえされないこともある
責任、依存関係、そしてインターフェース
¥ 自身より変更されないものに依存しなさい
上記はクラス「内」のメソッドにも当てはまる
パブリックメソッドは安定している
プライベートメソッドは不安定
4.3 パブリックインターフェースを見つける
アプリケーション例:自転車旅行会社
見当をつける
思い浮かぶであろうクラス: 参加者、旅行、行程、自転車、整備士など
-> 頭に思い浮かぶであろう理由は、アプリケーションにおいてデータと振る舞いを兼ね揃えた名詞を表すから
これらを「ドメインオブジェクト」と呼ぶ
ドメインオブジェクトは永続化する = 明確
ドメインオブジェクトは大きくて目に見える現実世界のものを表す
最終的にデータベースへ
ドメインオブジェクトは簡単に見つけることができるが、アプリケーション設計をする上で中心となるものではない
こだわりすぎると無理な振る舞いをさせがち
ドメインオブジェクト同様に必要なものの、はるかに見えにくい他のオブジェクトが存在する
それらを見つけるためには、オブジェクトではなくオブジェクト間で交わされるメッセージに注意を向ける
PCの前に座る前にまずこれらに対する見当をつける = コードを書かなくても設計を検討できる・相互理解を深める簡潔で低コストな方法
シーケンス図を使う
オブジェクトとメッセージを実験するための低コストな方法はシーケンス図
基本的な設計の質問を、
「このクラスが必要なのは知っているけれど、これは何をすべきなんだろう」 から
「このメッセージを送る必要があるけれど、だれが応答すべきなんだろう」へ変えることがキャリア転向への第一歩
¥ オブジェクトが存在するからメッセージを送るのではない。メッセージを送るためにオブジェクトは存在する
「どのように」を伝えるのではなく「何を」を頼む
送り手の望みを頼むメッセージ(何を) と 受け手にどのように振る舞うかを伝えるメッセージ(どのように) は大きく異なる
この違いを理解することがパプリックインターフェースを持つ再利用可能なクラスを作るための鍵となる
パブリックインターフェースが小さい = 他のところから依存されるメソッドがわずかしかない
コンテキストの独立を模索する
オブジェクトはコンテキストを持つ(e.g. Tripはprepare_bicycleメッセージに応答できるMechanicオブジェクトを持ち続ける)
オブジェクトが要求するコンテキストはオブジェクトの再利用がどれだけ難しいかに直接関わる(e.g. prepare_bicycleメッセージに応答できるMechanicオブジェクト持たない限りTripを再利用できない)
最も良いのはオブジェクトがそのコンテキストから完全に独立していること
相手が誰かも、何をするかも知らずに他のオブジェクトと共同作業できるオブジェクトは再利用しやすい
具体的なテクニックは依存オブジェクトの注入
他のオブジェクトを信頼する
× 具体的なコンテキストを持ってしまっているオブジェクト「私は自分が何を望んでいるか知っているし、あなたがそれをどのようにやるかも知っているよ」
○ 抽象的なコンテキストに依存するオブジェクト「私は自分が何を望んでいるか知っているし、あなたがあなたの担当部分をやってくれると信じているよ」
後者では、コードを「修正」ではなく「拡張」することで問題への対応が可能。
最もかんたんな変更方法はコードの追加である。ただし追加するコードはそれ自体が変更が容易なものとする
要するに上記の条件を満たすやり方が他のオブジェクトを手放しで信頼し、自身をコンテキストに縛り付けないこと
オブジェクトを見つけるためにメッセージを使う
オブジェクトを発見するためにシーケンス図を使う
新しいオブジェクトが見つかった場合、そこにメッセージを送る必要性があったために発見されたということになる
4.4 一番良い面(インターフェース)を表に出すコードを書く
インターフェースこそが、全てのテストよりも、他のどんなコードよりも、アプリケーションを定義し、その将来を決定づけるものである
明示的なインターフェースを作る
依存できるものを伝えることは設計者の責任
クラスを作るときは毎回インターフェースを宣言する
code:_
パブリックインターフェースに含まれるメソッドは次のようにあるべき
明示的にパブリックインターフェースだと特定できる
「どのように」よりも、「何を」になっている
名前は、考えられる限り、変わり得ないものである
オプション引数として、ハッシュをとる
プライベートなインターフェースについても同じような意図をもって設計する
誤解する余地がないほど明確にする
プライベートメソッドは全くテストしない or するとしてもパブリックメソッドから隔離する
private, protected などのキーワード
柔らかい障壁のようなものであり、絶対的な制限ではない
「将来の」プログラマーが持つ情報よりも、今の自分がより良い情報を持っていると信じているという意思表示に用いる
今の自分が不安定だと考えているメソッドを、将来のプログラマーに不用意に使われないようにするために用いる
コンテキストを最小限にする
パブリックインターフェースを構築する際は、そのパブリックインターフェースが他者に要求するコンテキストが最小限になることを目指す
「何を(what)」と「どのように(how)」の違いを常に念頭に置く
パブリックメソッドを作る際は、メッセージの送り手が、クラスがどのようにその振る舞いを実装しているかを知ることなく、求めているものを得られるようにつくる
4.5 デメテルの法則
デメテルを定義する
code:_
デメテルは・・・
そこへメッセージを「送る」ことができるオブジェクトの集合を制限する
3つ目のオブジェクトにメッセージを送る際に、異なる型の2つ目のオブジェクトを介すことを禁じる
「直接の隣人にのみ話しかけよう」「ドットは1つしか使わないようにしよう」
第5章 ダックタイピングでコストを削減する
オブジェクト指向の目的 = 変更にかかるコストを下げること
メッセージを設計の中心として捉える + 厳密に定義されたパブリックインターフェースを構築する = ダックタイピング
ダックタイプはいかなる特定のクラスとも結びつかないパブリックインターフェース
クラスをまたぐインターフェースはアプリケーションに大きな柔軟性をもたらす
クラスへの高コストな依存がメッセージへのより寛容な依存に置き換えられるから
5.1 ダックタイピングを理解する
手続き型言語ではデータ型の知識があればアプリケーションの中身の振る舞いを予想できる
オブジェクト指向ではオブジェクトの一連の想定はパブリックインターフェースへの信頼
あるオブジェクトが他のオブジェクトの型を知っていればどのメッセージにそのオブジェクトが応答できるかを知っていることになる
必ずしもあるオブジェクトがただ1つのインタフェースにだけ応答すると想定しなければならないわけではない
Rubyのオブジェクトはテーマに沿ってマスクを変える仮面舞踏会の参加者のようなもの
オブジェクトの使い手はそのクラスを気にする必要はなく、気にすべきものでもない
ダックタイプはクラスを跨ぐ型
ダックタイプのパブリックインターフェイスは契約を表すもの
この契約は明示的かつ適切に文書化されていなければならない
ダックタイプを説明する最善の方法はダックタイプを使わない場合どうなるかを検討すること
ダックタイピングの影響
オブジェクトのクラスについての不明瞭さを大目に見る能力 = 自信を持った設計者であることの証明
オブジェクトをそのクラスではなくあたかも振る舞いによって定義されているかのように扱えるようになれば、表現力や柔軟性を備えた設計という新たな領域に足を踏み入れたも同然
5.2 ダックを信頼するコードを書く
設計上で難しいことはダックが必要であると気づくこと
そのインタフェースを抽象化すること
隠れたダックを認識する
隠れたダックの存在を示唆するもの
クラスで分岐するcase文
kind_of?とis_a?
responds_to?
ダックを信頼する
クラスで分岐するcase文, kind_of?, is_a?, responds_to? いずれも「あなたが誰だか知っている。なぜならあなたが何をするかを知っているから」と言ってるも同然
この知識は共同作業するオブジェクトへの信頼を欠如していることを表す
何かのオブジェクトを見逃している
見逃しているオブジェクトが具象クラスではなくダックタイプであるという事実は全く重要ではない
重要なのはそのインターフェース
5.3 ダックタイピングへの恐れを克服する
静的型付けによるダックタイプの無効化
動的型付けを恐るプログラマーはコード内でオブジェクトのクラスを検査する傾向にある
それは動的型付けの効果を削ぐ・タックタイプの利用を不可とする
静的型付けと動的型付け
コンパイラが型を検査しなかったら絶対に実行時のエラーが起こるとは限らない
型がなくてもプログラマーはコードを理解できる。オブジェクトのコンテキストからその型を推測できる
一連の最適化がなくてもアプリケーションの動作が致命的に遅くなることはない
コンパイラは不慮の型エラーからプログラマを救わない
変数を新しい型にキャストできる全ての言語には脆さがある(キャストした途端に白紙に戻る)
型エラーを防ぐのはコンパイラではなくプログラマの知力
第6章 継承によって振る舞いを獲得する
6.1 クラスによる継承を理解する
継承とは根本的に「メッセージの自動委譲」の仕組み
理解されなかったメッセージに対して、転送経路を定義するもの
あるオブジェクトが受け取ったメッセージに応答できなければほかのオブジェクトにそのメッセージを委譲する
明示的にメッセージを委譲するコードを書く必要はない
2つのオブジェクト間の継承関係を定義するだけで転送は自動的に行われる
クラスによる継承ではサブクラスを作ることでこれを実現している
6.2 継承を行う箇所を識別する
継承を選択する
もしスーパークラスが複数あれば(多重継承なら)、サブクラスからの転送処理が煩雑になる
多くのオブジェクト指向言語は単一継承(サブクラスは親となるスーパークラスを1つしか持つことができない)によってこの問題を回避している
ダックタイプはクラスを横断する
ダックタイプがコード共有のために使うのはモジュール
6.3 継承を不適切に適用する
6.4 抽象を見つける
抽象的な振る舞いを昇格する
リファクタリングするときの戦略: 「もし間違っているとしたら何が起こるだろう」と問いかける
空のスーパークラスを作って抽象的な部分をそこに押し上げる
抽象を全く見つけられないで何も昇格させられないなら、サブクラスが同じ振る舞いを必要とした時に可視化され、発見することができる
全てのサブクラスが同じ振る舞いにアクセスできるようにするためにはコードの複製かスーパークラスへの昇格
コードを複製するのは論外
このリファクタリングを逆に行う(具象的な部分のみを新しいサブクラスに押し「下げる」)
具象的な振る舞いの一部を誤って置き去りにしてしまう恐れがある
残された具象的な振る舞いが全てのサブクラスに当てはまることなどあり得ない -> サブクラスは継承の基本的なルールに違反してしまう(信頼できない階層構造に)
信頼できない階層構造はそれと関わるオブジェクトに階層構造の癖を知るように強制する
code:_
一般的なルール:
一般に、新たな継承の階層構造へとリファクタリングをする際は、抽象を昇格できるようにコードを構成すべきであり、具象を降格するような構成にすべきではない
リファクタリングするときの戦略: もし間違っているとしたら何が起こるだろう -> 間違っていた「とき」何が起こるだろう?
具象から抽象を分ける
属性値はアクセサとセッターによって表現されるべきであり、ハードコードされた値で表現されるべきではない
テンプレートメソッドパターンを使う
全てのテンプレートメソッドを実装する
コードを一見するだけでは明確に把握することができない要件をサブクラスに課すのは問題がある
「テンプレートメソッドパターンを使うどのクラスも、その送信するメッセージのすべてに必ず実装を用意するようにする」という簡単なルールで痛みを和らげる
それが単に NotImplementedError を raise するものであっても読む人には有益な文書を、そうでない人には有益なエラーを提供できる
6.5 スーパークラスとサブクラス間の結合度を管理する
結合度を管理することは重要である。
強固に結合されたクラス同士は互いに結着し、それぞれを独立に変更することは不可能である。
結合度を理解する
他のクラスについての知識を持つということは、例に漏れず、依存を作り、依存はオブジェクトを互いに結合する
superを送っているメソッドは結合を作る
superを送るのに失敗したり、送り忘れたりすると辛いデバッグになる(エラーはエラーの原因とは遠く離れたところで一度に起こることがあるため)
superを用いる階層構造でのコードのパターンではサブクラスは自身が行うことだけではなく、スーパークラスとどのように関わるかまで知っておくことが要求される
自身が果たす特化についてサブクラスが知っているのは意味が通るが、抽象スーパークラスとどのように関わるかまで知るように強制するのはいくつもの問題を引き起こすことになる
プログラマは正しい特化を加えることはそう簡単には忘れないがsuperを送るのは簡単に忘れてしまう
サブクラスがsuperを送るとき、これは事実上、そのアルゴリズムを知っているという宣言である = サブクラスはこの知識に「依存」している
アルゴリズムに変更があればたとえサブクラスで特化していること自体に変更はなかったとしてもサブクラスがとたんに壊れてしまうこともある
フックメッセージを使ってサブクラスを疎結合に
superに関する問題はスーパークラスが代わりに「フック」メッセージを送るようにリファクタリングすれば回避できる
code:_
フックメッセージ:
サブクラスがそれに合致するメソッドを実装することによって情報を提供できるようにするための専門メソッド
6.6 まとめ
継承は関連する型に関する問題を解決する
関連する型 = 山ほど共通の振る舞いを持つものの、いくつかの面においては相違もある型
継承によって共有されるコードを隔離でき、共通のアルゴリズムを抽象クラスに実装できる
抽象的なスーパークラスを作るために一番いい方法 = 具象的なサブクラスからコードを押し上げること
正しい抽象を特定するのが最も簡単なのは存在する具象クラスが少なくとも3つあるとき
抽象スーパークラスはテンプレートメソッドパターンを使うことでその継承者(サブクラス)に専門的に特化するように促す
フックメソッドによってsuperの送信を強制せずともサブクラスがスーパークラスに特化を提供できるようになる
階層構造の各階層間の結合度が低減し、変更に強くなる
適切に設計された継承の階層構造はたとえアプリケーションについてあまり詳しくないプログラマーであっても新たなサブクラスを用いて簡単に拡張できる
第7章 モジュールでロールの振る舞いを共有する
単に継承を使うと失敗し得る問題がある
e.g.
すでに存在する2つのサブクラスの性質を組み合わせる必要がある(Rubyではクラスは1つしか継承できない)
クラスによる継承で解決できる問題には必ず他の解決方法もあるが、どの設計手法にもコストが伴う
-> アプリケーションの費用対効果を最大限に高めるためにそれぞれの解決法をよく検討し、納得した上で取捨選択をする必要がある。その際に検討することは得られるであろう利益と相対的なコスト
継承の代替の例示 -> モジュールを使って共通のロールを定義する
7.1 ロールを理解する
code:_
ロール:
以前には関連のなかったオブジェクト同士に共通の振る舞いを持たせなければならない。
この共通の振る舞いはクラスと直交する。これがオブジェクトが担う「ロール(役割)」
アプリケーションに必要なロールは設計じに明らかになるものが大半であるが、コードを書いていて初めてわかるものもある
もともと無関係だったオブジェクトが共通のロールを担うようになるとオブジェクトは互いに関係を持つようになる
この関係は継承ほど明確なものではないが、依存関係が生じることには変わりない
ロールを見つける
共有されるコード(ダックタイプ)の理想的な状態 = 1ヶ所で定義されているものの「ダックタイプとして振舞うことでロールを担いたい」と思っているどんなオブジェクトからも利用できるようになっている
Rubyでのモジュールは共通のロールを担うための完璧な方法
責任を管理する
どんな改善が必要かはコードのパターンからわかる
e.g.
どの「メッセージ」を送れば良いかを知るためにクラスを確認する
どの「値」を使えばいいかを知るためにクラスを確認する
不必要な依存を取り除く
ダックタイプを見つけて、特定のクラス名への依存を除去する
オブジェクト自身に自信を語らせる
StringUtils.empty?(some_string) みたいなコードは実に馬鹿げている
文字列を管理するために独立したクラスを使うことは明らかに冗長である
文字列は各々オブジェクトなのだからそれ自身が振る舞いを持ち、自己管理をすべきである
具体的なコードを書く
抽象を抽出する
is-a と behaves-like-a の違いは間違いなく重要であり、どちらを選ぶかでそれぞれ及ぼす影響が異なる
メソッド探索の仕組み
オブジェクトの把握するメソッドがオブジェクト自身のクラスに置いてあるということは、クラスのインスタンスは全てが同じメソッドの集合を共有できるということ
1箇所に存在すべき定義を1箇所に存在させることができる
ロールの振る舞いを継承する
クラスによる継承を深くネストした階層構造を作り、さらにモジュールを階層構造のあちこちにインクルードすると理解もデバッグも拡張も全くできないコードになる
正しい使い方を学ぶ最初の一歩は継承可能なコードを的確に書くこと
7.2 継承可能なコードを書く
code:_
アンチパターン1:
オブジェクトがtypeやcategoryという変数名を使いどんなメッセージをselfに送るかを決めているパターン
アンチパターン2:
メッセージを受け取るオブジェクトのクラスを確認してからどのメッセージを送るかをオブジェクトが決めているパターン
抽象に固執する
抽象スーパークラス内のコードを使わないサブクラスがあってはならない
全てのサブクラスでは使われないけど一部のサブクラスでは使うというようなコードはスーパークラスに置くべきではない
※モジュールでも同様
契約を守る
サブクラスは「スーパークラスと置換できることを約束する」
これは契約であり守らねければならない
置換できる場合 = オブジェクトが期待通りに振る舞うときで、かつ、サブクラスがスーパークラスのインターフェースに一致するように「期待される」ときのみ
サブクラスはスーパークラスのインターフェースに含まれるどのメッセージが来ても応えられるべきであり、同じ種類の入力を取り、同じ種類の出力を行わなければならない
他のオブジェクトに自身の型を識別させ、自身の扱いや何が期待できるかを決めさせることはどんなことがあっても許されない
code:_
リスコフの置換原則(LSP):
型がTであるオブジェクトxについて証明できる属性をq(x)と表す。このとき型がSであるオブジェクトyについてq(y)が真となる。
ただし、型S は型Tの派生型であるとする
= システムが正常にあるためには、派生型は、上位型と置換可能でなければならない」
テンプレートメソッドパターンを使う
テンプレートメソッドパターンを使うと抽象を具象から分けることができる
抽象コードでアルゴリズムを定義し、抽象を継承する具象ではテンプレート化されたメソッドをオーバーライドすることで特化を行う
テンプレートメソッドはアルゴリズムのうち変化する場所を表す
前もって疎結合にする
継承する側でsuperを呼び出すようなコードを書くのは避ける
代わりにフックメッセージを使う
フックメソッドはsuperを送ることによって発生する依存の問題を回避することはできるが、階層構造の階層が隣接していなければ使えない
階層構造は浅くすべき
階層構造は浅くする
フックメソッドが使えなくなるという他にも階層構造を浅く保つべき理由はたくさんある
階層構造 = 深さと幅を保つピラミッド
深さ = オブジェクトから頂点までの間にあるスーパークラスの数
幅 = 直下のサブクラスの数
浅く狭い階層構造は簡単に理解できる
浅く広い構造は若干複雑
深い階層構造はメッセージを解決するための探索パスがとても長くなる
大量の依存を持つ
その上依存しているものが各々いつか変わる可能性がある -> エラー発生へ
7.3 まとめ
オブジェクトが共通するロールを担うためには、振る舞いを共有する必要があり、そのためにはRubyではモジュールが役に立つ
モジュールに定義されたコードは、どんなオブジェクトにも追加できる
モジュール内のメソッドはインクルード先のクラスでの探索パスに追加される
モジュールでもインクルード先でsuperを送信することを強制することは避けるべき
オブジェクトがどこか別のところに定義された振る舞いを取り込む時はそれがスーパークラスであれモジュールであれ、リスコフの置換原則を守らなければならない
第8章 コンポジションでオブジェクトを組み合わせる
code:_
コンポジション: 組み合わされた全体が、単なる部品の集合以上となるように、個別の部品を複雑な全体へと組み合わせる(コンポーズする)行為
e.g.
音楽(音符がコンポーズされている)
ソフトウェアも音楽のようにコンポジションによって構築できる
シンプルで独立したオブジェクトをより大きく、より複雑な全体へと組み合わせる
コンポジションにおいてはより大きいオブジェクトとその部品が「has-a」の関係によって繋げられる
自転車にはパーツがある。包含する方のオブジェクトが自転車で包含されるのはパーツ
コンポジションの定義と切り離せないのは自転車はただパーツを持つだけではなく、インターフェースを介してパーツと情報交換をするということ
ここの部品(パーツ)はロールであり、自転車自体はそのロールを果たすほかのどんなオブジェクトとも喜んで協力する
8.1 自転車をパーツからコンポーズする
継承をリファクタリングする際にコンポジションを用いることがある
Bicycleクラスを更新する
Parts階層構造を作る
コンポジションを用いるリファクタリングによって包含するオブジェクト(この場合Bicycle)がスッキリする(Bicycle特有のコードがいかに少なかったが浮き彫りになる)
8.2 Partsオブジェクトをコンポーズする
Partsオブジェクトをもっと配列のようにする
8.3 Partsを製造する
以下の知識量はかなり多く、アプリケーションのあちこちに簡単に漏れる = 残念で不必要なもの
アプリケーションのどこかで何らかのオブジェクトがPartオブジェクトの作り方を知っていなければならない
特定のオブジェクトを知っている(オブジェクトがマウンテンバイクであること)
個別のパーツは多岐にわたり、いくつもあるものの、有効なパーツの組み合わせはほんの一部だけ
さまざなま自転車を記述し、その記述をもとになんらかの方法で、正確なPartsオブジェクトをどの自転車にも製造できれば全てはより簡単になる
単純な二次元配列なんかで組み合わせを記述できる
PartsFactoryを作る
ほかオブジェクトを製造するオブジェクトはファクトリー
ファクトリーは上記以上でも以下でもないのだから、この語句にアレルギーを持つべきではない
PartsFactoryを活用する
8.4 コンポーズされたBicycle
パーツを包含しているオブエクトはそれらをロールとして捉えている
Column 集約 - 特殊なコンポジション
コンポーズされたオブジェクトはパーツから成り立つ
コンポジションが記述するのはhas-aの関係
e.g. 食事には前菜があり、大学には学部がある
パーツは明確に定義されたインターフェースを介してコンポーズされたオブジェクトと相互作用することが想定される(コンポーズされたオブジェクトはロールを担うオブジェクトのインターフェースに依存する)
e.g. 食事はインターフェースを介し、前菜と相互作用をする
前菜の役割を果たしたい新たなオブジェクトはこのインターフェースを実装するだけで済む
広義のコンポジションはではほとんどの場合において一般的なこの2つのオブジェクト間の「has-a」関係以上のことを意味することはない
正式な定義ではコンポジションは「has-a」関係を持ち、かつ包含される側のオブジェクトが包含する側のオブジェクトから独立して存在し得ないものを指す
e.g. 料理が食べられてしまえば前菜はなくなるし、大学がなくなれば学部もなくなる
「集約」とコンポジションは似ているが違う
集約では包含される側のオブジェクトの存在が独立していることが異なる
e.g. 大学は学部を持ち、学部には教授がいるがある学部が完全に消えてしまっても教授たちは存在し続ける
コンポジションと集約の違いがコードに影響を及ぼすこともある
8.5 コンポジションと継承の選択
継承はメッセージを自動的に委譲する仕組み
「オブジェクトを階層構造に構成するコストを支払う代わりに、メッセージの委譲は無料で手に入れられる」
コンポジションはそれらの利点を逆転させる代替案
コンポジションではオブジェクトは独立して存在しているため、お互いについて明示的に知識を持ち、明示的にメッセージを委譲する必要がある
継承の方がより良い解決法であるとはっきり言い切れないときはコンポジションを使うべき
コンポジションが持つ依存は継承が持つ依存よりもはるかに少ないものであるため
継承がより良い選択肢であるのは、継承が低いリスクで高い利益を生み出してくれるとき
継承による影響を認める
継承の利点
継承は TRUEのうちRUEにおいて優れた効果をもたらす
継承階層の頂点に近いところで定義されたメソッドの影響力は広範囲に及ぶ
階層の高さがテコの役割を果たし、変更は継承ツリーを波及する -> 正しくモデル化された階層構造は「合理的」
継承を使った結果得られるコードは「オープン・クローズド」
階層構造は拡張には開いており、修正には閉じているので新たなサブクラスを追加する時に既存のコードへの変更は全く必要ない -> 「利用性が高い」
正しく書かれた階層構造を簡単に倣うことができ、プログラマーはそのパターンを踏襲する責任を負うべき -> 模範的
継承のコスト
継承が適さない問題に対して誤って継承を選択してしまうこと
問題に対して継承の適用が妥当であったとしても、自分が書いているコードが他のプログラマーによって全く予期していなかった目的のために使われるかもしれないこと
誤った継承はTRUEいずれも満たさない
継承では「自分が間違っている時、何が起こるだろう」という問いかけが特別重要な意味を帯びる
コンポジションの影響を認める
コンポジションの利点
いいコンポジション実装では「責任が単純明快であり、明確に定義されたインターフェースを介してアクセス可能な小さなオブジェクト」が自然といくつも作られる
これらのオブジェクトは簡単に理解できて変更が起きた場合に何が起こるか明確 -> 見通しがいい
インターフェースを守れば既存部品の亜種は簡単に追加できる -> 合理的
適切な粒度のオブジェクトは容易に抜き差し可能・入れ替え可能 -> 利用性が高い
コンポジションのコスト
ここの部品すべては「見通しが良い」ものであったとしてもコンポーズされたオブジェクトは多くのパーツに依存するため、全体はそうではないかもしれない
関係の選択
継承とは特殊化
継承が最も適しているのは過去のコードの大部分を使いつつ、新たなコードの追加が比較的少量のときに、既存のクラスに機能を追加する場合
振る舞いがそれを構成するパーツの総和を上回るのならコンポジションを使うべき
is-a関係に継承を使う
behaves-like-a関係にダックタイプを使う
設計においてすべきこと
ロールが存在することを認識し、そのダックタイプのインターフェースを定義すること
そしてそのインターフェースの実装を可能性のある全ての担い手に提供すること
has-a関係にコンポジションを使う
is-aとhas-aの違いが継承とコンポジションの選択をする際の核心
オブジェクトがパーツを持てば持つほどコンポジションでモデル化されるべきであるという可能性が高まる
8.6 まとめ
コンポジションによって小さなパーツを組み合わせて、パーツの総和を上回るようなより複雑なオブジェクトを作ることができる
コンポーズされたオブジェクトは、新たな組み合わせへと簡単に編成できる
自然とオブジェクトが小さくなる
理解しやすく、テストしやすい
コンポジション・クラスによる継承、モジュールを使った振る舞いの共有はコード構成のための互いに競合するテクニック
これらを適切に選択できるのがいい設計者
第9章 費用対効果の高いテストを設計する
変更可能なコードを書くことを実践するために必要なこと
オブジェクト指向設計を理解していること
コードのリファクタリングに長けていること
価値の高いテストを書く能力があること
9.1 意図を持ったテスト
テストの真の目的 = コストの削減 = 設計の真の目的
以下のケースではテストは無価値
テストがない場合のバグ修正・ドキュメンテーションコスト < テストの記述・メンテナンス・実行
テストの意図を知る
バグを見つける
欠陥やバグを開発プロセスの初期段階で見つけることは大きな利益となる
バグは早く見つけるほど修正が簡単
テストを活用して早い段階でコードを正しくできる
仕様書となる
設計の決定を遅らせる
テストがインターフェースに依存している場合はその根底にあるコードを奔放にリファクタリングできる
コードの修正によってテストを書き直す必要はない
抽象を支える
設計の欠陥を明らかにする
テストのセットアップに苦痛が伴うのであればコードはコンテキストを要求しすぎている
一つのオブジェクトをテストするために他のオブジェクトをいくつも引き込まなければならないならそのコードは依存を持ちすぎている
しかし、テストにコストがかかるからといってアプリケーションの設計がまずいわけではない
適切に設計されたコードに、できの悪いテストを書くことは技術的に十分にありえる
何をテストするかを知る
ほとんどのプログラマーはテストを書きすぎている
テストからより良い価値を得るための1つの単純な方法はより少ないテストを書くこと
どのテストも一度だけ、それも適切な場所で行うようにする
オブジェクト指向のアプリケーションはブラックボックスの集まり
どのオブジェクトもブラックボックスであるかのように扱う
他のオブジェクトが知ることを許される知識に制約が課される
公開される知識についても境界を突き通すメッセージに限られる
適切に設計されたオブジェクトはとても強固な境界を持つ
外部にいる誰も内側を覗くことはできない
他のあらゆるオブジェクトの内部を意図的に無視することが設計の核心にある
オブジェクトをオブジェクトが応答するメッセージそのもの、かつそれだけであるかのように扱うことで変更可能なアプリケーションを設計することができる
テストはオブジェクトの境界に入ってくるか出ていくメッセージに集中すべき
いつテストをするかを知る
テストの方法を知る
9.2 受信メッセージをテストする
オブジェクトの受信メッセージはパブリックインターフェースであり、そのシグネチャと戻り値にアプリケーションの他のオブジェクトが依存する
これらのメッセージはテストを必要とする
使われていないインターフェースを削除する
誰も送りもしないメッセージを実装しているならテストは不要。メッセージ自体を削除する。
パブリックインターフェースを証明する
受信メッセージをテストするにあたり第一に求められること = 考えられ得る全ての状況において正しい値を返すことを証明すること
テスト対象のオブジェクトを隔離する
オブジェクトのクラスに縛り蹴られないようにする
インターフェースへ依存する
クラスを使って依存オブジェクトを注入する
ロールとして依存オブジェクトを注入する
テストダブルをつくる
code:_
テストダブル:
ロールの担い手を様式化したインスタンスであり、テストのみで使われるもの
夢の世界に生きる
テストダブルの使い方を誤ると、アプリケーションがはっきり間違っているにも関わらず、テストはパスしてしまうという状況となってしまう
テストを使ってロールを文書化する
9.3 プライベートメソッドをテストする
プライベートメッセージが送られてくるところを、テスト対象オブジェクトというブラックボックスの外からは見ることができない
理想的な設計の汚れのない世界においてはそれらをテストする必要はない
しかし現実はそうはいかない
テスト中ではプライベートメソッドを無視する
プライベートメソッドのテストは必要ない
プライベートメソッドはパブリックメソッドによって実行され、パブリックメソッドはすでにテストされている
プライベートメソッドは不安定 = アプリケーションコードの変わりやすいところへの結合
テストも頻繁に変わるならメンテナンスのコストが利益を上回ってしまう
プライベートメソッドのテストをすることで、他のメソッドがそれらを間違って使ってしまうことになる
テスト対象クラスからプライベートメソッドを取り除く
プライベートメソッド自体も作らないようにする
プライベートメソッドを大量に持つオブジェクトは責任を大量に持ちすぎた設計となっている可能性が高い
プライベートメソッドのテストをするという選択
code:_
プライベートメソッドをテストする際の大まかなルール:
プライベートメソッドは決して書かないこと。書くとすれば、絶対にそれらのテストをしないこと。
ただし、当然のことながらそうすることに意味がある場合を除く
9.4 送信メッセージをテストする
送信メッセージは「クエリ」 or 「コマンド」
クエリメッセージはそれらを送るオブジェクトにのみ問題となる
コマンドメッセージは、アプリケーション内の他のオブジェクトから見える影響を及ぼす
クエリメッセージを無視する
副作用のないメッセージはクエリメッセージとして知られる
テストではselfに送られたメッセージは無視されるべき
外に出て行くクエリメッセージもまた、無視されるべき
コマンドメッセージを証明する
「モック」は振る舞いのテスト
メッセージが何を戻すかの表明をするのではなく、メッセージが送られるという期待を定義する
モックはメッセージに対してそれを受け取ったことの記憶しかしていない
モックはメッセージが送られたことを証明するためのものであり、結果を返すのはテストの進行に必要な時のみ
前もって依存オブジェクトを注入していれば簡単にモックに置き換えられる
9.5 ダックタイプをテストする
ロールをテストする
ロールテストを使ったダブルのバリデーション
9.6 継承されたコードをテストする
継承されたインターフェースを規定する
サブクラスの責任を規定する
サブクラスの振る舞いを確認する
サブクラス間でそれぞれが要件を満たすことを証明するためにテストを共有する
スーパークラスによる制約を確認する
固有の振る舞いをテストする
具象サブクラスの振る舞いをテストする
抽象スーパークラスの振る舞いをテストする
抽象スーパークラスのテストは難しい
リスコフの置換原則を活用し、テスト専用のサブクラスを作るときにはそれらのサブクラスにもサブクラスの責任のテストを適用する
9.7 まとめ
テストはなくてはならないもの
テストがなければアプリケーションは理解もできず、安全に変更することもできない
もっとも良いテストとは、対象のコードと疎結合であり、全てに対し一度だけテストをし、それが適切な場所で行われているもの
https://scrapbox.io/files/6365028a98c420001d3e9ad7.png https://amzn.to/3Tdrpgm