オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方
オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 を読破したので、なぜこの本を読んだのか、感想、読んだときのメモを残しておきます。
なぜ読んだのか
ネットサーフィンをしているとオブジェクト指向に迷える子羊に対して強い人がこう言っているのをよく見かけます。
“オブジェクト指向設計実践ガイドを読め”
と。それが答えです。
自分はこの本を読んで、
- 何かこの設計はダメだしっくりこない
- この設計は何かしらんが書きやすいぞビューチフルだ
こういった今までの経験でごまかしていた「何か」の部分に対して、「あぁ、ここはXXだから〇〇だよね」と具体的に考えられるようになりました(ふわっとしていたものに名前をつけられるようになるのってスゴく大事なことだと自分は思うのですよ)し、感覚で書いていたいくつかの部分においては間違っていなかったという安心感を得られました。
正直こういった技術書はいつも 積読/つまみ食い で読み切ったことはなかったのですが、はじめて読破できました。なんなら2回読み直したし。
図書館で集中して読むぞ pic.twitter.com/RgixiWNw8s
— しまぶ (@shimabox) May 5, 2019
自分が思うに、
- プログラマー初級者
- 「そっか、こうやって書くとこうなるからスッキリするんだな」という学びを得られる
- プログラマー中級者
- 「そっか、あれはこういうことだったんだな」という気づきを得られる
- プログラマー上級者
- 「うんうん。そういうことそういうこと。」という確信を得られる
といったようにどのような層においても得られるものが十分ある本だと思います。
なお、Ruby
を使って説明されていますが、プログラミングを少しかじっていればRubyを知らなくても十分理解できる内容です。※ペチパーの自分でもわかった気になれたのだから間違いない
というわけで、この本は自信を持って良書だとオススメできます。
感想
自分は単一責任の重要性は何となくわかっているつもりで、なにかを作る際にはそこを意識してなるべく細かく部品を作るのですが、そうなってくると必ず部品を組み合わせる役割
を持つクラス(取りまとめ役と呼びます)が必要になってきます。
そして、この取りまとめ役を作るのが一番むずかしいと感じていて、気づいたら
- if/case文
- setter/getter
- 微妙なUtilクラス
が乱立され依存関係がどんどん複雑になっていき、ふと気づいたら、今はちゃんと動くけど、この先できれば触りたくない忌み嫌われるクラス を作ってしまうということが多々ありました。
その点について本書は、段階を踏まえながらそういった依存関係の管理をどのように行っていけばいいのかということを教えてくれます。
オブジェクト指向は 依存関係を管理すること だと言い切っていますし。
依存関係の管理には、継承, ダックタイプ, 委譲 をうまく使えということが書かれているわけですが、どういったケースでどれが有効なのかを分かりやすく説明してくれています。
(まぁ、委譲推しなわけですけども)
変更されにくいものを抽象にして変更されるものを具象に、自身より変更されないもの(抽象)に依存するようにする。
これを意識した上で上記のテクニックを使って依存関係の管理をしていくわけですね。
何が変更されて何が変更されにくいのかを嗅ぎ取るのは少し経験がいるかもしれませんが、この「抽象をみつける」ということに関しては常に意識してやっていくべきです。
他には特に、
- 「このメッセージを送る必要があるけれど、だれが応答すべきか」と考える
- メッセージを送るためにオブジェクトは存在する
これには頭を殴られた感覚に近いものがあって、自分の中の価値観がガラッと変わった気がします。
※4章で書かれています
今までは、クラスがあってメソッドがあってメソッドでやり取りをする。そういった考えがありました。(いやこれが普通でしょ)
ですが、この本では 「まずはメッセージだ、メッセージを見つけろ」 と言っているわけです。
なので自分は難しいものを考えるとき、まずは簡単な絵(シーケンス図)を書くようになりました。
これでオブジェクト同士の会話がスムーズに行われているか俯瞰的に見るわけです。
ぎこちない会話をしているものは必ず後々厄介になったりします。端的に説明できるのがGoodです。
こうすると誰かに設計意図を伝えるときにも説明しやすくなって一石二鳥ってわけですね。
ほかにも、必要以上に相手のことを知らないようにしつつも、信頼して使う。みたいなことも言っていて、メッセージの件といい、これは現代社会の縮図か?と感じられることもしばしばありました。
もしかしたらオブジェクト指向を突き詰めると現代社会も上手に生きのびられるのかもしれない。
というわけで、この本を読んで感銘を受けたポイントを絞ると以下の3つになります。
- 単一責任
- 依存関係の管理
- クラスではなくメッセージに基づく視点を持つ
繰り返しになってしまいますが、クラスではなくメッセージに基づく視点を持つ が一番この本を読んで良かったと思える気づきでした。
この辺を意識し精進してまいります。
では、最後に本書を読んだ際のメモを書いて終わりにします。
本書を読んだ際のメモ
6章まではスラスラっと読めますが、7章あたりから難しくなってきます(7章 < 8章 < 9章)。
読んだ時間もメモったので残しておきます。
箇条書きでメモを残していますが、本書ではもちろんソースコードも添えられてわかりやすく記述されているので、もし興味の出た方はぜひ実際に手にとって読んでみてください。損はしないと思います。
1章 オブジェクト指向設計 (約30分)
- アプリケーションの要件変更は避けられない
- 要件の変更はプログラミングにおける摩擦料と重量といえる
- よく練られた計画に影響を及ぼす
- だからこそ、設計は重要
- オブジェクト指向とは「依存関係を管理すること」
- 実用的な設計とは未来を推測するものではなく
- 未来を受け入れるための選択肢を保護するもの
- 選択してしまうではなく、動くための余地を設計者に残すもの
- 設計の目的は「あとにでも」設計をできるようにすること
- 第一の目標は変更コストの削減
- 設計の知識を少し得たころこそ危険
- 執拗に設計するようになる
- 不適切な場所に原則を適用し存在しないところにパターンを見出す
- これはわかりみが強い。。
- 「アジャイルはいいぞ」という話が続く
- BUFDのディスり
- BUFD … 詳細設計のこと。コーディングとテストの前に大きな詳細設計を行うソフトウェア開発メソッドを表す用語。
- BUFDの設計資料は、はじめはアプリケーション開発のロードマップに用いられるが最終的に最後の責任の押し付け合いの中で使われる言葉になる
- アジャイルが成功するかどうかは、シンプルで適応性があり柔軟性があるコードにかかっている
- オブジェクトは「データ」と「振る舞い」を持つ
- 設計理論を理解し正しい時期と正しい分量で適用する
2章 単一責任のクラスを設計する (約1時間)
- オブジェクト指向設計のシステムの基礎は「メッセージ」
- 第一にやるべきことは「シンプルであれ」と主張すること
- 「いますぐに」求められる動作を行い、かつ「あとにも」かんたんに変更できるようにモデル化する
- 設計とはアプリケーションの可変性を保つために技工を凝らすこと
- 完璧を目指すための行為ではない
- 「変更がかんたんであること」
- 見通しがよい(Transparent)
- 合理的(Reasonable)
- 利用性が高い(Useful)
- 模範的(Exemplary)
- それぞれの頭文字をとってTRUE
- TRUEなコードを書くためにそれぞれのクラスが単一の責任をもつよう徹底する
- クラスはできる限り最小で有用なことをするべき
- リム(ただのメモです)
- 車輪の外周の環状部分
- 変更がかんたんなアプリケーション
- 再利用がかんたんなクラスから構成される
- かんたんなクラス
- 着脱可能なユニット
- 変更がかんたんなアプリケーションは「つみき」が詰まった箱
- 必要な部品を選べる
- 2つ以上の責任を持つクラスはかんたんには再利用できない
- クラスが単一責任か見極める
- クラスの持つメソッドを質問に言い換える
- 意味を成す質問になっているか
- 一文でクラスを説明できる
- 「それと」や「または」が含まれないか
- 「凝集度」という言葉を使いこの概念を表す
- クラスの持つメソッドを質問に言い換える
- クラスの責任に迷ったら
- 「今日何もしないことの将来的なコストはどれだけだろう?」と問う
- 将来的なコストがいまと変わらなければ決定は延期
- コードが嘘をついているときには、その嘘を信じ、広めてしまうプログラマーに注意
- 「いますぐ改善」と「あとで改善」間の緊張は常に存在する
- 現時点での要件と未来の可能性の相互間のトレードオフを理解し
- コストが最小になるように決断を下す
- 変更を歓迎できるコードに構成しておくこともできる
- データではなく「振る舞い」に依存する
- インスタンス変数の隠蔽
- アクセサメソッドで包む(
attr_reader:
など) - データを振る舞いに変える
- アクセサメソッドで包む(
- データ構造の隠蔽
- 例えば配列を
Struct
の配列(小さくて軽量なオブジェクト)にする - 複雑さを自身から見えないところに隠す
- 例えば配列を
- インスタンス変数の隠蔽
- メソッドもクラスのように単一の責任を持つべき
- 役割が何であるか質問できて一文で責任を説明できるように
- 単一責任のメソッドがもたらす恩恵
- 隠蔽されていた性質を明らかにする
- コメントをする必要がなくなる(メソッド名でわかる)
- 再利用を促進する
- ほかのクラスへの移動がかんたん
- 責任の隔離の決定に迷っても決定を「あとで」できる力をとっておく
- 変更可能でメンテナンス性の高いオブジェクト指向ソフトウェアへの道のりは単一責任のクラスからはじまる
3章 依存関係を管理する (約1時間)
- せわしなく動作するアプリケーションでも数歩下がって全体を見ればパターンを見出すことができる
- オブジェクトに望まれる振る舞いは以下のうちのどれか
- 自身が知っている
- 継承している
- そのメッセージを理解するほかのオブジェクトを知っている
- 「知っている」というのは同時に依存も作り出す
- ほかのクラスの名前、ほかのクラスのメソッド、その引数
- クラスが知るべきことは自身の責任を果たすために必要十分なことのみであるべき
- 2つ以上のオブジェクトの結合が強固なとき、それはあたかも1つのユニットであるかのように振る舞う
- オブジェクト間の結合(CBO:Coupling Between Object)
- 疎結合なコードを書く
- 依存オブジェクトの注入
- 依存性の注入と同義
- 依存オブジェクトの注入
- 依存を隔離する
- インスタンス変数の作成を分離
- 別メソッドに分けてインスタンスを生成する
- 依存を明らかにする効果
- 脆い外部メッセージを隔離
- 「外部メッセージ」は「self以外に送られるメッセージ」
- 外部メッセージに依存している部分を別メソッドに出してselfに送られるメッセージに依存するようにする
- 引数の順番への依存を取り除く
- 初期化の際の引数にハッシュ、キーワード引数(PHPにも欲しい!!) を使う
- キー名への依存は出るがポジティブ(ドキュメント代わりになったり) なものである
- 明示的にデフォルト値を設定する
boolean
を考えるとfetch
メソッドを使ったほうがいい- デフォルト値を返すメソッドを用意して
merge
させるのもあり
- 複数のパラメータを用いた初期化を隔離する
- シグネチャを変更できない場合
- インスタンス生成のラッパー(モジュール)を用意してそこに隠す
- ファクトリーパターン
- インスタンス変数の作成を分離
- 自身より変更されないものに依存するようにする(依存方向の選択)
- 「要件の変わりやすさ」と「依存されているオブジェクトの数」の組み合わせを4つのグリッドに分ける
A
- 抽象領域
- 変更は起こりにくいが変更が起こると広範囲に影響を及ぼす
- 依存されている数 : 多い
- 要件が変わる可能性 : 低い
B
- 中立領域
- 変更は起こりにくく副作用もわずかしかない
- 依存されている数 : 少ない
- 要件が変わる可能性 : 低い
C
- 中立領域
- 変更は起こりやすいが副作用はわずかしかない
- 依存されている数 : 少ない
- 要件が変わる可能性 : 高い
D
- 危険領域
- 変更は起こる。そして、変更はそこに依存するものに伝わっていく。
- 依存されている数 : 多い
- 要件が変わる可能性 : 高い
- 適切に設計されていれば、A, B, C の領域にクラスは集まる
- Aに含まれるのは抽象クラスやインターフェースであるべき
- Cには具象クラスが含まれやすい
- Dにあるクラスが表すものは将来への危険信号
- かんたんな要求が悪夢のようなコーディングに化ける
- 抽象は「いかなる特定のインスタンスからも離れている」
4章 柔軟なインターフェースを作る (約1時間15分)
個人的には一番勉強になった章になります。
- クラスが何を「する」かではなく、何を「明らかにする」かとなっていると悩む場面が出てくる
- パブリックインターフェース (ここでのインターフェースは「クラス内」にあるもの)
- クラスの主要な責任を明らかにする
- 外部から実行される
- 気まぐれに変更されない
- 他者がそこに依存しても安定
- テストで完全に文章化されている
- プライベートインターフェース
- クラス内のパプリックインターフェースで定義されているメソッド以外はすべてプラーベートインターフェース
- 実装の詳細に関わる
- ほかのオブジェクトから送られてくることは想定されていない
- どんな理由でも変更されえる
- 他者がそこに依存するのは危険
- テストでは言及されないこともある
- 最初のテストを書くためには、何をテストしたいかについての考えが必要
- アプリケーションのユースケースを満足させるために必要な「オブジェクト」と「メッセージ」の両方についてまず見当をつける
- シーケンス図がとても役に立つ
- ユースケースでの
名詞がオブジェクト
になりアクションはメッセージ
になる - 設計の重点がクラスからメッセージになる
- 「このメッセージを送る必要があるけれど、だれが応答すべきか」と考える
- メッセージを送るためにオブジェクトは存在する
- 「どのように」を伝えるのではなく「何を」を頼む
- インターフェースのサイズ(数)が小さくなる
- クラス自身が望むことに集中させる
- ほかのオブジェクトを信頼する
- メッセージを使いオブジェクトを見つける
- インターフェースがアプリケーションの将来を決定づける
- 明示的なインターフェースを作る
- 「どのように」よりも「何を」になっている
- 名前は考えられる限り変わりえないものである
- オプション引数としてハッシュをとる
public
,protected
,private
は不安定さを伝える手段- この考え、自分には全くなかった
- この章で言っているプライベートって可視性の話ではない??
- きちんとキーワードもあるみたいだけど。。
デメテルの法則
- となりのとなりの人に何かを頼んだらアカン
- となりの人に頑張らせるのはあり
- 異なる型を2つ以上つなげない
- あそこに、いまここで欲しい振る舞いがあることを知っている
- それは「それをどのように手に入れたらいいのかを知っている」と言っているようなもの
- 「どのように」よりも「何を」に徹する
- 違反が見つかったらパブリックインターフェースが欠けているオブジェクトがあるのではないかというヒントになる
5章 ダックタイピングでコストを削減する (約45分)
- オブジェクトの使い手はそのクラスを気にする必要はなく、また、気にするべきでもない
- オブジェクトが「何である」かではなく「何をする」か
- ダックタイプのパブリックインターフェースは契約を表す
- シーケンス図はそれが描いているコードよりも簡単でなければいけない
- そうでなければ設計の何かがおかしい
- 具象クラスとメソッドをいくつも知っているのは危険
- ダックタイプ(抽象)を用いて具象を隠す
- ポリモーフィズム
- 難しいのは、ダックタイプが必要であることに気づくことと、そのインターフェースを抽象化すること
- 次のものはダックで置き換え可能
- クラスで分岐するcase文
kind_of?
とis_a?
responds_to?
- kind_of?, is_a?, responds_to? はRuby限定かな
- これらが存在するのは未特定のダックの存在を示唆している
- 「あなたが誰だか知っている。なぜならば、あなたが何をするか知っているから」と言っているも同然
- パブリックインターフェースを発見できていないオブジェクトを見逃している
- ダックを作り信頼する
- ただし、ダックを作るかはその時の判断による
- コストが下げられるかどうか
- 動的型付けを恐れるプログラマーはコード内でクラスを検査する傾向にある
- 型検査がどんどん追加されると柔軟性を損ないさらにクラスに依存することになる
- ダックタイプはコードが安全に依存できる安定した抽象を明らかにする
- RubyってInterfaceの型ないの??
- ググると
module
でNotImplementedError
を使う方法もあるみたい(これがアリかはおいといて)
- ここからは静的型付き言語vs動的型付け言語の構図
- メタプログラミングの価値についてとか
6章 継承によって振る舞いを獲得する (約1時間30分)
- 継承とは根本的に「メッセージの自動委譲」をする仕組み
- 継承を使うと有益な箇所を識別する
- 「あなたが誰なのか知っている。」なぜなら私は「あなたがすることを知っているのだから。」
- この知識は依存であり、変更のコストを上げる
- 継承は「共通の振る舞いを持つものの、いくつかの面については異なる」という強く関連した型の問題を解決する
- 埋め込まれた型を見つける
- 継承が効果を発揮するのは以下の2つが常に成立しているとき
- “一般 – 特殊”(汎化 – 特化) の関係をしっかりと持っている
- 正しいコーディングテクニックを使っている
- Rubyには
Abstract
はない- 他社を信頼する性質から(へぇ~)
- サブクラスをたった1つだけ持つ抽象的なスーパークラスは全く意味がない
- 具体的な要求が出てきてから考える(YAGNI)
- 階層構造を作るのが遅くなると
- 大量の重複したコードを持つクラスを書くことになる
- かといって、階層構造を作る決断が早すぎても
- 正しい抽象概念を特定するための情報を十分に持っていないかもしれないというリスクを受け入れることになる
- 情報が3種類以上くらい揃ったなら正しい抽象を見つけられる可能性が上がる
- “3回以上同じことをするなら自動化” に似てる
- 最善な判断を。前に進むことは恐れない。
- 設計の戦略を決める際、一般的に有用な方法
- 「もし間違っていたら、何が起こるだろう」と考える
- 具象から抽象を見つけて昇格させる
- 抽象に具象的な振る舞いが残らないように
- いったん、具象からはじめて抽象を見つける
- 抽象から具象への降格失敗は影響が大きい
- 抽象に具象的な振る舞いが残らないように
- 新たな継承の階層構造へとリファクタリングをする際のルール
- 抽象を昇格できるようにコードを構成する
- 具象を降格するような構成にしない
- 設計者の決断に伴う2つのコスト
- 実装コスト
- 間違いだとわかったときの変更コスト
- このコストの両方を考慮する
- テンプレートメソッドパターンが有効
- 子クラスに実装させたいメソッドには
raize NotImplementedError
を書いて文章化する - そもそも
Abstract
があればいい気がするけど
- 子クラスに実装させたいメソッドには
- ただし、
super()
があるとちょっと危険- 自分はもちろん、親のことも知っていることになる
- たとえ親でも他のクラスを知ることになる
- サブクラスがアルゴリズムの知識を知っている
- フックメッセージを使ってサブクラスを疎結合にする
- フックメッセージを用意して実行タイミングの制御をスーパークラスに任せる
- サブクラスはそれに合致するメソッドを実装し情報を提供する
- サブクラスからアルゴリズムの知識を取り除く
- 親を信頼する
- フックメッセージを使ってsuper(親)を知らずにできないか考える
7章 モジュールにロールの振る舞いを共有する (約1時間30分)
- 既に存在する2つ(以上)のサブクラスの性質を組み合わせるケースを考える
- 関連のなかったオブジェクトに共通の振る舞いを持たせる
- 共通の振る舞い -> ロール(役割)
- 共通/共有で発生する依存関係は最小になるように
- Rubyでは
モジュール
が共通のロールを担うための完璧な方法となる- PHPだとtraitになるのかしら
- ただしモジュールを使うと設計はいっそうと複雑さを増す
- オブジェクトは自身を管理すべき
- 自身の振る舞いは自身で持つ
- オブジェクトBに関心があるときに、オブジェクトAの知識が求められてはならない
- Aの知識を使わずともBについて知れるべき
- 抽象の部分を抜き出してモジュール化する
- 継承
- 〜である(is-a)
- モジュール
- 〜のように振る舞う(behaves-like-a)
- どちらも自動的なメッセージの委譲に頼る
- Rubyでは受け取ったメッセージが見つからなければ、新しいメッセージ(
method_missing
)が最初にメッセージを受け取ったオブジェクトに渡される- 次は
method_missing
の探索が行われる
- 次は
extend
の説明- Ruby初心者の自分には正直よくわからなかったのでコードが欲しかった。。
- モジュールは協力であるがゆえに慣れないうちは危険が伴う手法
- 継承の階層構造、モジュールの利用性、メンテナンス性はそのままコードの質となる
アンチパターン
- オブジェクトがtypeやcategoryといった変数名を使い、どんなメッセージをselfに送るかを決めている
- -> クラスによる継承を考える
- メッセージを受け取るオブジェクトのクラスを確認してから、どのメッセージを送るかをオブジェクトが決めているパターン
- -> ダックタイプを見落としている
抽象に固執する
- 抽象スーパークラス内のコードを使わないサブクラスがあってはならない
- 一部のサブクラスでしか使われていないとか
- モジュールも同様
- オブジェクトの癖を知ることになる -> 知識への依存
契約を守る
- サブクラスはスーパークラスと置換できることを約束する
- リスコフの置換原則
- サブクラスはスーパークラスのインターフェースに含まれるどのメッセージがきても応えられるべき
- スーパークラスと置き換えられなければスーパークラスの型かどうか疑わしくなる
- こうなると階層構造全体が正しいのかも疑わしくなる
- テンプレートメソッドを使って何が変化するもの(具象)で、何が変化しないもの(抽象)なのかを明確に決める
- superは依存を生むので、できれば避ける
- フックメソッドを使う
- ただし、子供の子供となった場合など呼び出さざるをえないことも
- 階層構造は浅くする
- 階層が深いと大量の依存を生む
- 中間層のクラスは軽くあしらわれやすい
- ここへの何気ない変更が思わぬエラーを生む
- わかりみが強い
- 階層構造は浅く狭く
- 暗黙の契約を避ける
- 「オブジェクトは自身が主張するとおりに振る舞うべき」
8章 コンポジションでオブジェクトを組み合わせる (約2時間)
- 継承からコンポジションへ
- コンポジションとは個別の部品を複雑な全体へと組み合わせる(コンポーズする)行為
- コンポーズ … 合成する
- 音楽もコンポーズされたものの一例
- コンポジションにおいてはより大きいオブジェクトとその部品が has-a の関係によってつなげられる
- 部品は「ロール(役割)」であり、包含するオブジェクトはインターフェースを介して情報交換をする
- UMLで黒いひし形 ◆ は「コンポジション」を示す
- 1 は1つ、1..* は1つ以上
- 例だと、Arrayっぽく扱えるように
Forwardable
を継承してEnumerable
をインクルード - 部品が増えると、必要な部品、部品の作り方を知らないといけなくなる
- 例だと、
Parts
1..*Part
となったとき、Parts
を作る際にPart
の知識が必要Parts
(コンポーズされたオブジェクト)
- Factoryを使う
- 例だと、
OpenStruct
の導入をしている - ここで言っているコンポジションは正式な定義とは違う
- 正式には「has-a」関係を持ち、かつ、包含される側のオブジェクトが包含する側のオブジェクトから独立して存在しえない
- 上でいうと、Partは独立して使えないということになる(Partsが無いと存在しない)
- ここで言っているコンポジションは集約
- 「has-a」の関係を持つが、包含される側のオブジェクトの存在は独立できる
- クラスによる継承は「コード構成のテクニック」
- オブジェクトの階層構造に構成するコストを払う代わりにメッセージの委譲は無料で手に入れられる
- コンポジションはそれらのコストと利点を逆転させる代替案
- コンポジションではオブジェクトは独立して存在する
- その結果、オブジェクトはお互いについて明示的に知識を持ち、明示的にメッセージを委譲する必要がある
- 独立して存在できるが、それは明示的なメッセージ委譲のコストを払ってのこと
- 継承とコンポジションで悩んだらコンポジションを優先に考える
- コンポジションが持つ依存は継承が持つ依存よりもはるかに少ない
- 継承を選択するときは継承が低いリスクで高い利益を生みだすとき
- 継承の利点は「オープン・クローズド(Open-Closed)」なコードを得やすい
- 正しくモデル化された階層構造であれば
- 継承のコスト
- 継承が適さない問題に対して適応されがち
- 予期していなかった目的のために使われがち
- 「自分が間違っているとき、何が起こるだろう」という問いかけが大事になる
- 依存の集まりを伴うので修正の影響が大きい
- 書いたコードをどのような人たちが使うかという予想に基づいて調整されるべき
- コンポジションの利点
- 責任が単純明快
- 明確に定義されたインターフェースを介してアクセス可能
- 上記を持つ小さなオブジェクトが自然といくつも作られる
- 適切にコンポーズされたオブジェクトは「利用性が高く」なる
- 抜き差し可能で入れ替え可能なコンポーネント
- コンポジションのコスト
- 多くのパーツに依存する
- 明示的にどのメッセージを誰に委譲するかを必ず知っていないといけない
- 継承が最も適しているのは過去のコードを大量に使いつつ新たなコードの追加が比較的少量のときに、既存のクラスに機能を追加する場合
- 改修案件だとこればっかりになっている気がする。。
is-a
関係に継承
を使うbehaves-like-a
関係にダックタイプ
を使うhas-a
関係にコンポジション
を使う- 自身の設計技術を改善する鍵
- 上記のテクニックを使う
- 自身の失敗を快く受け入れる
- 過去の設計の決断にとらわれない
- 容赦なくリファクタリングをしていく
9章 費用対効果の高いテストを設計する (約3時間15分)
- 変更可能なコードを書くための3つのスキル
- オブジェクト指向設計の理解
- 優れたリファクタリングのスキル
- 効果的なテストを書く能力
- テストをする真の目的はコストの削減
- テストは唯一信用できる設計の仕様書となる
- 一度は把握していた説明を思い出させてくれるテストを書く
- テストによって設計の決定を安全に遅らせることができる
- 意図的にインターフェースに依存するようにする
- 任意の有益な度合いの抽象を作ることができる
- 対象コードの設計の欠陥をあらわにする
- テストのセットアップに苦痛が伴うのであれば、コードはコンテキストを要求しすぎている
- 依存関係を持ちすぎている
- 設計がまずければテストも難しい
- が、テストにコストがかかるからといって設計がまずいというわけでもない
- コストを下げるテストを実現するためにはアプリケーションとテストの両方を適切に設計する必要がある
- 可能な限り低いコストでテストの恩恵をすべて得るには
- 問題となることのみをテストする
- 疎結合のテストを書く
- テストからより良い価値を得るための1つの単純な方法
- より少ないテストを書くこと
- テストはオブジェクトの境界に入ってくる(受信する)か、出ていく(送信する)メッセージに集中するべき
- オブジェクトの内部を意図的に無視する
- つまり、パブリックインターフェースに定義されるメッセージを対象としたテストを書く
- 送信クエリ(副作用の全くないもの)メッセージは送り手でテストを書く必要はない
- 受け手側でテストを書く
- 送信コマンド(副作用を持つ)メッセージは送り手側でテストを書く
- メッセージが送られた回数、使われた引数など
- 何をテストすべきかのガイドライン
- 受信メッセージはその戻り値の状態がテストされるべき
- 送信コマンドメッセージは送られたことがテストされるべき
- 送信クエリメッセージはテストを書かなくてもよい
- テストを最初に書くとオブジェクトには、はじめから多少の再利用性をもたらす
- そうでもしないとテストは書けない
- わかりみが強い
- だからこそ、初級の設計者はテストファーストでコードを書いたほうが有益
- かといって、適切に設計されたアプリケーションが作られる保証にはならないので注意は必要
- Rubyにおいてのメインストリームのテストフレームワークは
MiniTest
とRSpec
がある - テスト駆動開発(TDD)と、振る舞い駆動開発(BDD)がある
- TDD
- 内から外へのアプローチをとる
- ドメインオブジェクトからのテストからはじまり、隣接するレイヤーのテストで再利用
- BDD
- 外から内へのアプローチをとる
- アプリケーションの境界でオブジェクトを作り、内向きに入っていく
- モックが必要になる
- アプリケーションのオブジェクトを大きく2つのカテゴリーに分けて考える
- 1つは自身がテストするオブジェクト -> 「テスト対象オブジェクト」
- もう1つは、そのほかのものすべて
- このカテゴリーのものについて可能な限り無知であるべき
- テスト中に利用可能な情報はテスト対象オブジェクトを見ることによってのみ得ることができるものだけであると考える
- これはなかなかハードルが高いなぁ。。
- 使われていないインターフェース(コード)は削除する
- 未来を推測したニオイが漂う実装
- 復旧するより残し続けるコストのほうが高い
- 内部に隠されている密な結合はテストを壊しやすくする
- これはアプリケーションにもいえる
- 本来テストしたいクラスの振る舞いにだけ注力したい
- 依存クラスを注入する
- クラスを使って依存オブジェクトを注入するのか、ロール(役割)として依存オブジェクトを注入するのか
- 具象クラスを渡すのではなく、抽象的なロールとして渡す
- テストダブル -> スタブ
- 夢の世界に生きる現象が起きる(これはわかる。起きる。)
- テストダブルで置き換えられているクラスのインターフェイスが変わってもテストでは通ってしまう
- アプリケーションではエラー、テストはPASSする -> 異なる世界が作られる
- ロールに対するテスト(
assert_respond_to
)を書く方法もある(ロールの文章化)が、悲痛なほど不完全- 「9.5 ダックタイプをテストする」にて解決策あり
※1
- 「9.5 ダックタイプをテストする」にて解決策あり
- プライベートメソッドは無視する
- プライベートメソッドはパブリックメソッドのテストで実行されているはず
- そもそもプライベートメソッドを大量に持っているオブジェクトは責任を大量に持ちすぎている可能性がある
- プライベートメソッドをテストしない勇気がないのなら新たなオブジェクトに切り出すのも方法としてある
- ただし、本当に役に立つのは新しいインターフェースが確かに安定しているときだけである
- プライベートメソッドは不安定なものであるから、信頼性が増すことはない
- リファクタリングを経て、安全性が増したのであれば別クラスに出してもよい
- 送信クエリメッセージを無視する
- 書いてもいいけどメンテナンスコストを上げる
- 送信クエリメッセージはそれを受け取る側でテストをする
- 送信コマンドメッセージ
- メッセージを送ったことを証明する
- 「モック」を使う
9.5 ダックタイプをテストする
- ロールがインターフェースを実装しているかのテストをモジュール化する
- ロールを使う側はロール(モック)に対してインターフェースを呼んでいるか(メッセージを送っているか)をテストする
※1
の解決策- ロールがパブリックインターフェースを実装しているかのテストをモジュール化する
- ロールを使う側のテストを書く
9.6 継承されたコードをテストする
- リスコフの置換原則を守っているか
- #共通の契約がそなわっているか確認するテスト(
assert_respond_to
)をモジュール化する- 親のテスト、子のテストでインクルード
- #サブクラスに求められる要件がそなわっているか確認するテストをモジュール化
※2
する- 子のテストでインクルード
- 上記2つ#を行うことで標準から外れていないかの確認になる
- 委譲を行ってサブクラスにおいて特化している振る舞いのテストを書く
- スーパークラスの知識がサブクラスのテストに漏れてこないように注意
- スーパークラスの振る舞いの確認は、スーパークラスを継承したサブクラスを別途用意(スタブ)してテストを書く
※2
もインクルードする(サブクラスの責任もきちんとテストする)
おわりに
最後の最後に言わせてください。
読んだことない人は読んでくれ!!
以上。