オブジェクト指向設計実践ガイド〜Rubyでわかる進化しつづける柔軟なアプリケーションの育て方〜が良書で、「数年前の自分に他のオブジェクト指向の書籍をモヤモヤしながら何度も読むより、これ読んどけ!!」とオススメしたいと思ったので、紹介します。
難易度もほどほどで重要なことが丁寧に書かれているので、他のオブジェクト指向系の書籍を読んで「わかったような、わからないような...」って状態の人におすすめ!!
オブジェクト指向関連の書籍はサンプルコードがJavaで書かれていることが多いですが、この書籍はRubyで書かれているので、普段Rubyをメインで扱っている人には必読です。
こんな人におすすめ
- 駆け出しのRubyエンジニア
- オブジェクト指向がいまいちわかっていないRubyエンジニア
- 他のオブジェクト指向の書籍を読んだけど、モヤモヤが残っている人
目次
おすすめポイント イマイチな設計のRubyコードを改善していく本
この書籍はイマイチな設計のRubyで書かれたサンプルアプリーケーションを提示し、それを改善していくといった流れで書かれていて、「なるほど!こういった感じで書けばよいのか!」と納得しながら読みすすめることができます。
駆け出しのエンジニアがオブジェクト指向の本を読むと、「振る舞い」、「知っている」、「メッセージを送る」、「副作用」、「責任」とかとか独特な言葉が出てきて、理解するのが難しいと思います。
さらにサンプルコードがJavaだったりして、普段Rubyを使っていると脳内変換しないといけないみたいな。(Rubyだと明示的にインターフェースを定義しないなどの違いがありますよね)
その点、この書籍は題材がRubyなので
おすすめポイント
- 「Rubyで書くときにはどんな感じになるのか」を脳内変換せずに、本文の解説とサンプルコードを読みすすめることができるので、読みやすいし、頭に入ってきやすい!!
- 丁寧な説明と程よい難易度
オブジェクト指向設計実践ガイドで学べること
2章 単一責任のクラスを設計する
単一責任の原則(Single Responsibility Principle)は、「クラスが担う責任は、たったひとつに限定すべき」という原則。
- 2つ以上の責任を持つクラスは、簡単に再利用できない
- 単一責任の考えはクラス以外にも適用でき、メソッドも単一責任であるのが好ましい
- もし、余計な責任が混ざっていて簡単に取り除けなければ隔離する。隔離しておくことで、影響を最小限に抑え、あとで別のクラスに移しやすくなる。
単一責任か見極めるコツ
- クラスを1文で説明してみる
- 「それと」、「または」といった言葉が出てきたら単一責任でない可能性大
- クラスのメソッドを質問に置き換える
- 不自然な質問になっていたら怪しい。
- メソッドの役割が何であるか質問し、1文で説明してみる
3章
オブジェクトに変更を加えたとき、他のオブジェクトも変更せざるを得ないなら、片方に「依存している」と言える。
不必要な依存関係はコードの「合理性」を損なうため、疎結合なコードを書くべき。
- 「依存性の注入」(本書では、「依存オブジェクトの注入」)によって結合なコードにする
- 依存を取り除くのが難しい場合、他のクラスに依存しているコードを隔離する(ラッパーメソッドを作る)
- 自身よりも変更されないものに依存させる
- (変更の可能性が低いため)抽象化されたものに依存した方が安定する
4章 柔軟なインターフェースをつくる
- オブジェクトが何を知っているか(オブジェクトの責任)や、誰を知っているか(オブジェクトの依存関係)だけでなく、オブジェクトが互いにどのように会話するのかも設計で考慮することに含まれる。
- パブリックインターフェースを見つけるには、
- オブジェクト間で交わされるメッセージ(メソッド)を中心に考えるとよい(シーケンス図が有効)
- どんなメッセージ(メソッド)が必要かを決め、それをどこに(どのクラスに)送るかを決める
- 受け手に「どのように」振る舞うのかを伝えるのではなきく、送り手が「何を」望むのかを伝えるメッセージ(メソッド)にする
- 簡単なコンテキスト(オブジェクトが求める情報)を求めるオブジェクトにする。コンテキストを簡単にするには「依存性の注入」が有効
- デメテルの法則(LoD: Law of Demeter)を守る
- オブジェクトを疎結合にするためのコーディング規則
- メソッドチェーン時、3つ目のオブジェクトにメッセージを送る際に、異なる型の2つ目のオブジェクトを介するべきではない
- 遠くの振る舞いを得るためにいくつものオブジェクトを介する状況になったら、「必要なパブリックインターフェースが欠けていないか?」、「違反しているメソッド・処理部分が「何を」求めているのか?」を考えると良い
デメテルの原則については、https://tech-blog.rakus.co.jp/entry/20200701/programming がわかりやすい。
※ テクニック面では委譲を使ってドットを減らすことができる。Rubyの場合、delegate.rbやforwardable.rbやRailsが備えるdelegateメソッドが使える。ただ、見た目には違反を回避できているように見えるだけになってしまわないように注意。
5章
オブジェクト(変数の値)に何ができるかはオブジェクトそのものが決定する。これによりポリモーフィズム(多態性)を実現することができる。特定のメソッド(またはメソッド群)を実装したオブジェクトが必要な場合に、与えられたオブジェクトが必要とするメソッドのすべてを実装しているのであれば、その実装方法がどのようなものであれ(型宣言の有無、継承の経路などに関係なく)、それで良いという考え方である。
Wikipedia ダック・タイピング
- ダックタイピングは根底にある抽象をあらわす
- 単にパブリックインターフェースの取り決め
- ダックタイピングはパブリックインターフェースを特定のクラスから切り離し、何で「ある」かではなく、何を「する」かによって定義される仮想の方をつくる
- 本書のサンプルコードでいうと、
Preparer
はパブリックインターフェースで設計上の想像。このPreparer
インターフェースはprepare_trip
メソッドを実装することを期待している。つまり、prepare_trip
メソッドを実装しているオブジェクトであればPreparer
として扱える。 Rubyの場合コード上に明示的に記述しない仮想的なもの。(Go言語とかならinterface定義でコード上に明確現れる)
- 本書のサンプルコードでいうと、
次のようなコードを見かけたら、隠れたダックが存在する可能性大
- クラスで分岐するcase文、if文
kind_of?
とis_a?
responds_to?
6章 継承によって振る舞いを獲得する
- 抽象クラスは具象クラスがつくられるために存在。これが唯一の目的。
- 抽象クラスは具象クラス間で共有される振る舞いの格納場所を提供する
- 具象クラスがそれぞれに特化したものを用意する
- 継承が効果を発揮するのは次の2つが成立するとき
- 「一般ー特殊(汎化ー特化)の関係」を持っている
- 正しいコーディングテクニックを持っている
- 継承のリファクタリングのコツは、「振る舞いを具象クラスに降格させ、抽象的な振る舞いを昇格させる」
- 継承の難しさは、抽象から具象を厳密に分けることに失敗することで生じる。
- 具象的な振る舞いが抽象クラスに残っているよりも、抽象的な振る舞いが具象クラスに残っている方が解決しやすい。後者は他の具象クラスが同じ振る舞いを必要としたときに可視化されて、抽象化できる。
- 抽象クラスと具象クラスの結合度を意識する
- フックメソッドを使う。こうすることで、具象クラスは抽象クラスのアルゴリズムを知らずに済み、疎結合にできる。
7章 モジュールでロールの振る舞いを共有する
- 無関係だったオブジェクトが共通の振る舞いを必要とするロール(役割)を担う場合、モジュールを使って振る舞いを共有する
- 自身の振る舞いは自身で持つべき。オブジェクトBについて関心があるとき、オブジェクトBを知るためにオブジェクトAの知識が求められることがあってはいけない。
- 文字列(String)が空かどうかを知るのに、`StringUtils.empty?(str)` なんてやるのは馬鹿げている。Stringクラスに`empty?`が実装されているべき
- モジュールでもテンプレートメソッドパターンは有効
- 抽象スーパークラス内のコードを使わないサブクラスがあってはならない。一部のサブクラスでは使うというコードはスーパークラスに置くべきではない。
- リスコフの置換原則
- 「派生型は上位型と置換可能でなければならない」(スーパークラスが使えるところではサブクラスが使えなければならない)
8章 コンポジションでオブジェクトを組み合わせる
- コンポジションとは、組み合わせた全体が、単なる部品の集合以上となるように、個別の部品を複雑な全体へと組み合わせる(コンポーズする)行為
- 大きいオブジェクトとその部品は「has-a」の関係(AはBを所有している関係)
- 例. 「自転車ーパーツ」自転車はパーツを所有している
- クラスの継承では、振る舞いは複数のオブジェクトに分散され、それらのオブジェクトはメッセージの自動的な委譲によって正しい振る舞いが実行される。一方、コンポジションではオブジェクトは独立して存在し、お互いについて明示的に知識を持ち、明示的にメッセージを委譲する必要がある
- 一般的に、コンポジションで解決できるものは、継承よりもコンポジションを使うことを優先すべき。コンポジションが持つ依存は継承が持つ依存よりも少ないから。
- コンポジションを使うと自然と小さなオブジェクトがいくつも作られるようになる→責任が単純明快で見通しが良くなる
- その一方、組み合わせた全体の動作は複雑になるかもしれない。
継承を使うと良い場合
- 「is-a」の関係のときに継承を使うと見通しが良い
- だたし、階層構造が大きくなる、大幅な拡張が必要になるときには代替案を考えるべき
コンポジションを使うと良い場合
- 「has-a」の関係のとき。BicycleはPartsを持つ(Bicycles have-a Parts)
- 「is-a」と「has-a」の違いが継承とコンポジションの選択を決断する際の重要ポイントとなる
ダックタイプを使うと良い場合
- さまざまなオブジェクトが共通のロールを担いうが、そのロールがオブジェクトの主な責任出ないときに使う。「behaves-like-a」の関係にあるとき。
- 自転車がスケジュール(予約)可能のように振る舞う(bicycle behaves-like-a schedulable)としても、bicycle is-a 自転車
- または、必要性が幅広いとき。いくつものオブジェクトが同じロールを担いたいとき。
- ロールには、インターフェースのみから構成されるものもあれば、共通の振る舞いを共有するものもある
- ロールについては、ロールを担うオブジェクト側の視点ではなく、ロールを担うオブジェクトを保持する側の視点で考えると良い。
- 「スケジュール可能なオブジェクト(Schedulable)」を保持するものは、ロールを担うオブジェクトがSchedulableのインターフェースを実装し、Schedulableの契約を守ることを期待する。すべての「Schedulable」はこれらの期待を満たさなければならない。
9章 費用対効果の高いテストを設計する
テストする意味
- バグを見つける
- 仕様書となる
- 設計の決定を遅らせる
- テストがインターフェースに依存している場合、その対象のコードはリファクタリングしやすくなる。そのため設計の決定を遅らせてもあとで改善できる
- 抽象を支える(テストがあることで安全に安心して抽象化することができる)
- 設計の欠陥を明らかにする
- テストのセットアップが大変な場合、コードがコンテキストを要求しすぎ
- テストのために、他のオブジェクトをいくつも引き込む必要があるなら、依存関係を持ちすぎ
- テストを書くのが大変なら、再利用が難しい
テスト時に意識すること
- パブリックインターフェースをテストする
- プライベートインターフェースをテストしても、アプリケーション全体の正しさの証明にならないし、クラスをリファクタリングするたびに壊れるのでコストが高い。
- メソッド内で別のメソッドを呼び出しているとき、それが送信クエリメッセージ(副作用がないメソッド呼び出し)ならテスト不要、送信コマンドメッセージ(副作用あり)なら送られたことをテストする
- ダックタイプのテスト
- ロールが担っているオブジェクトがパブリックインターフェースを実装していることをテストする。共通のテストを書いて各オブジェクトのテストでインクルードするとよい
- 呼び出し元では、正しいインターフェースが使われていることをテストする
- ダブルが特定のインターフェースを実装していることを期待する場合、そのダブルに対しても、インターフェースを実装しているかテストする。このときのテストは実際にそのロールを担っているオブジェクトのテストと共通化させておく
- 継承されたコードのテスト
- 継承の階層構造に属するすべてのオブジェクトが継承の契約を守っていることを証明する。つまり、リスコフの置換原則を守っているかを確認する。共通して実装されているはずのインターフェースのテストをスーパークラス、サブクラスに書く
- サブクラス固有の振る舞いをテストする
- スーパークラスの振る舞いをテストする。スーパークラスのインスタンスを用意するのが難しい場合、テスト用のサブクラスを作る
感想
難易度もほどほどで重要なことが丁寧に書かれていて、プログラマーになって1,2年目にこの本を読んでおけば良かったなと思うくらい良書でした。
他のオブジェクト指向系の書籍を読んで「わかったような、わからないような...」って状態の人におすすめです!!