はじめに
オブジェクト指向について勉強するため、オブジェクト指向設計実践ガイドを読むことにしました。
前回は「オブジェクト指向でなぜつくるのか第3版」を読んでオブジェクト指向の基礎的なところを抑えられたと思うので、今回は実際のアプリケーション開発でどのようにオブジェク指向を使うのかという実践的なところを学ぶため、この本にしました。
第1章 オブジェクト指向設計
1.1 設計の賞賛
- 永く動くアプリケーションであればあるほど、変更を加えるのが大きな問題となる
- 設計が貧弱だとオブジェクト間の依存関係が邪魔をして、変更が困難になる
- 設計の目的は後にでも設計をできるようにすることであり、そのための目標は変更コストの削減
1.2 設計の道具
設計原則(SOLID)
- オブジェクト指向設計の5つの原則をSOLIDという
- この原則に従えば、コードを改善できる
設計(デザイン)パターン
- オブジェクト指向設計には、原則に加えてパターンもある
- それぞれのパターンについて理解し、どのパターンを使うのか適切に選ぶことが大切
1.3 設計の行為
- 最初の設計が十分でないと、その後の設計に失敗する
- アジャイルでは、最初にBUFD(全体の詳細設計)を作ることに意味はない
- アジャイルでオブジェクト指向設計を行う場合、「変更しやすいようにどのようなコード構成にするか」を考える
1.4 オブジェクト指向プログラミングのかんたんな導入
手続き型言語
-
文字列・数値・配列などのデータ型が予め用意されており、それらを使うだけ
- 複雑なデータ構造を定義する(あらかじめ定義されているデータ型を集め、名前のついた構成にする)ことはできる
- しかし、完全に新しい操作や、まったく新しいデータ型を作ることはできない
-
振る舞いとデータは完全に別
オブジェクト指向言語
- 振る舞いとデータを、1つのオブジェクトにまとめる
- プログラマー自身が新たなデータ型(オブジェクト)を作ることができる
第2章 単一責任のクラスを設計する
- クラスを使って、今すぐに求められる動作を行い、且つ後にも簡単に変更できるようアプリケーションをモデル化する
2.1 クラスに属するものを決める
- コードの書き方は知ってるけどどこに書けば良いかわからない段階では、メソッドを正しくグループ分けしてクラスにまとめることが重要
- 変更が簡単なようにコードを組成するには、TRUEであるべき
- 見通しが良い(Transparent)
- 合理的(Reasonable)
- 利用性が高い(Usable)
- 模範的(Examplary)
2.2 単一の責任を持つクラスをつくる
なぜ単一責任が重要なのか
- 何をするべきか整理されておらず、いくつかの責任が絡み合ったクラスだと、クラスの本来の目的とは関係ない理由で変更されてしまう
- 変更が加わるたびに、そのクラスに依存する全てのクラスを破壊する可能性があり、アプリケーションを予期せず壊してしまう
クラスが単一責任かどうかを見極める
- クラスの持つメソッドを質問に言い換えた時に、意味をなす質問になっているか
- 1文でクラスを説明できるか
- 「それと」や「または」が含まれていたら、単一責任ではない
2.3 変更を歓迎するコードを書く
データではなく、振る舞いに依存する
- 単一責任のクラスを作れば、どんな振る舞いでも一箇所のみに存在するようになる
- これがDRY「Don't Repeat Yourself」の概念
- インスタンス変数を隠蔽するには、メソッドで包む
- 隠蔽することで、予期せぬ変更がコードに影響を与えることを防ぐ
- 複雑なデータ構造も隠蔽する
- Rubyには、構造を包み隠すためのクラス
Struct
が用意されている
- Rubyには、構造を包み隠すためのクラス
あらゆる箇所を単一責任にする
- メソッドから余計な責任を抽出する
- クラス内の余計な責任を隔離する
- 責任がありすぎて混沌としているクラスは、それらの責務を別のクラスに分ける
第3章 依存関係を管理する
3.1 依存関係を理解する
依存関係を認識する
- オブジェクトが以下のものを知っている時、オブジェクトには依存関係がある
- 他のクラスの名前
- selef以外のどこかに送ろうとするメッセージの名前
- メッセージが要求する引数
- それらの引数の順番
- 依存関係を管理して、それぞれのクラスが持つ依存を最低限にすることが重要
オブジェクト間の結合(CBO: Coupling Between Objects)
- 2つ以上のオブジェクトの結合が強固になると、1つのユニットのような振る舞いになる
- 1つのオブジェクトに変更を加えると、全てのオブジェクトに変更を加えないといけなくなる
- このように管理されていない依存関係を持つアプリケーションは、全てを変更するよりも一から書き直した方が簡単な状態になってしまう
3.2 疎結合なコードを書く
依存オブジェクトの注入
- クラス間の結合の切り離しを実現する
- 依存オブジェクトの注入を使うためには、「クラス名を知っておく責任や、そのクラスに送るメソッドの名前を知っておく責任が、どこか他のクラスに属するものではないかと疑える能力」が必要
依存を隔離する
- インスタンス変数の作成を分離する
- 他のクラスのインスタンスの作成を、
initialize
から分離して独自に定義したメソッド内で行う - この時にRubyの
||=
演算子を使って、インスタンスの作成を引き延ばすようにする
- 他のクラスのインスタンスの作成を、
- 脆い外部メッセージを隔離する
引数の順番への依存を取り除く
- 固定された順番の引数だと、その順番が変わった場合に修正箇所が膨大になる
- そこで、固定順の引数をオプションハッシュに置き換える
3.3 依存方向の管理
- クラスの振る舞いにアドバイスすると**「自身より変更されないものに依存しなさい」**となる
- このアドバイスは、以下3つの概念が元になっている
- あるクラスは、他のクラスよりも要件が変わりやすい
- 具象クラスは、抽象クラスよりも変わる可能性が高い
- 多くのところから依存されたクラスを変更すると、広範囲に影響が及ぶ
第4章 柔軟なインターフェースをつくる
- オブジェクト同士は、オブジェクトが持つ「インターフェース」を介して、会話している
4.1 インターフェースを理解する
- 他のオブジェクトが使えるよう晒されたメソッドによって、パブリックインターフェースが定義されている
4.2 インターフェースを定義する
- パブリックインターフェースとプライベートインターフェースが存在するのは、それが最も効率的に仕事ができる方法だから
- メソッドにパブリックやプライベートと印をつけることは、クラスの使用者に対してどのメソッドなら安全に依存できそうかを伝えている
- パブリックインターフェースは、クラスの責任を明確に述べる契約書である
パブリックインターフェース
- クラスの主要な責任を明らかにする
- 外部から実行されることが想定される
- 気まぐれに変更されない
- 他者がそこに依存しても安全
- テストで完全に文書化されている
プライベートインターフェース
- 実装の詳細に関わる
- 他のオブジェクトから送られてくることは想定されていない
- どんな理由でも変更され得る
- 他者がそこに依存するのは危険
- テストでは、言及さえされないこともある
4.3 パブリックインターフェースを見つける
見当をつける
- アプリケーションに求めらる要件をユースケースとする
- このユースケースを満足させるために必要なオブジェクトとメッセージの両方について、まず見当をつける
シーケンス図を使う
- オブジェクトとメッセージの実験をするためには、シーケンス図がうってつけの方法
- ユースケースでの名詞はシーケンス図ではオブジェクトになり、ユースケースでのアクションはシーケンス図ではメッセージになる
- 設計の重点がクラスからメッセージになる
- 設計の質問が「このクラスが必要なのは知ってるけど、これは何をすべきか」から「このメッセージを送る必要があるけど、誰が応答すべきか」に変わる
- オブジェクトが存在するからメッセージを送るのではなく、メッセージを送るためにオブジェクトが存在する
「どのように」を伝えるのではなく「何を」を頼む
- クラス間の会話が「どのように」から「何を」に変わると、パブリックインターフェースのサイズが一段と小さくなる
コンテキストの独立を模索する
- オブジェクトがそのコンテキストから独立していることが、ベストな状況
ほかのオブジェクトを信頼する
- オブジェクト同士が手放しで信頼しあえることが、オブジェクト指向設計の要
- 「私は自分が何を望んでいるかを知っているし、あなたがそれをどのようにやるかも知っているよ」
- 「私は自分が何を望んでいるかを知っていて、あなたが何をするかも知っているよ」
- 「私は自分が何を望んでいるかを知っていて、あなたがあなたの担当部分をやってくれると信じているよ」
オブジェクトを見つけるためにメッセージを使う
- 単一責任の原則を破っている場合、新しいオブジェクトが必要
- 新しいオブジェクトは、そこにメッセージを送る必要性があったために発見される
4.4 一番良い面(インターフェース)を表に出すコードを書く
明示的なインターフェースを作る
パブリックインターフェースに含まれるメソッドは、以下のようであるべき
- 明示的にパブリックインターフェースだと特定できる
- 「どのように」よりも、「何を」になっている
- 名前は、考えられる限り、変わり得ないものである
- オプション引数として、ハッシュをとる
Rubyのpbulic
・protected
・private
- この3つのキーワードは、全く異なる2つの用途で用いられる
- 1つ目は、どのメソッドが安定でどのメソッドが不安定か
- 2つ目は、どれだけメソッドが見えるか制御するため
- この3つのキーワードを使うことで、以下2つのことを伝えられる
- 「将来の」プログラマーが持つ情報よりも、今の自分の方がより良い情報を持っていると信じている
- 今の自分が不安定だと考えているメソッドを、将来のプログラマーに不用意に使われることは防がなければならないと信じている
4.5 デルメルの法則
- デルメルの法則は、オブジェクトを疎結合にするためのコーディング規則の集まり
- デルメルの法則に違反すると
- パブリックインターフェースの正確な定義と特定ができていない
- 実害がない場合もある
デルメルを定義する
- 「直接の隣人にのみ話しかけよう」「ドットは1つしか使わないようにしよう」
- 3つ目のオブジェクトにメッセージを送る際、異なる型の2つ目のオブジェクトを介することを禁じる
違反を回避する
- 委譲を使うと違反を回避できる
- Ruby→
delegate.rb
・forwardable.rb
- Ruby on Rails→
delegate
メソッド
- Ruby→
デルメルに耳を傾ける
- デルメルが伝えたいのは「委譲をもっと使いましょう」ということではない
- 「何を」を求めて、メッセージチェーンを再考する
- メッセージに基づく視点に移行してメッセージを見つければ、そのメッセージは自ずと何らかのオブジェクトのパブリックインターフェースとなる
- そのオブジェクトが何のオブジェクトなのかは、メッセージ自体が導いてくれる
第5章 ダックタイピングでコストを削減する
- ダックタイプは、いかなる特定のクラスとも結びつかないパブリックインターフェース
- オブジェクトは、そのクラスよりもその振る舞いによって定義される
- 「もしオブジェクトがダック(アヒル)のように鳴き、ダックのように歩くならば、そのクラスが何であれ、それはダックである」
5.1 ダックタイピングを理解する
- オブジェクトが何で「ある」かではなく、何を「する」か
- ダックタイプを見つけて実装することで、設計上の複雑な問題を解決できる
5.2 ダックを信頼するコードを書く
- 設計上で難しいのは、ダックタイプが必要であることに気づくことと、そのインターフェースを抽象化すること
- 隠れたダックを認識する方法として、以下のものはダックで置き換えることができる
- クラスで分岐するcase文
-
kind_of?
とis_a?
responds_to?
- これらは、未特定のダックの存在を示唆している
- つまり、まだパブリックインターフェースを発見できていないオブジェクトを見逃している
- ダックタイプを新たに作るかは、その時の判断による
- 設計の目的は、コストを下げること
- 不安定な依存が減るなら作ればいい
5.3 ダックタイピングへの恐れを克服する
- 静的型付けは、ダック対応を無効化する
- ダックタイピングは、動的型付けの上に成り立つ
第6章 継承によって振る舞いを獲得する
6.1 クラスによる継承を理解する
- 継承とは、根本的に「メッセージの自動委譲」の仕組み
6.2 継承を使うべき箇所を識別する
- 「『あなたが誰なのか』知っている。なぜなら私は『あなたがすること』を知っているのだから。」
- これはダックタイプを見つけた時のようにで、サブクラスを見つけることができる
- Rubyをはじめ多くのオブジェクト指向言語は、単一継承である
- スーパークラスはサブクラスをいくつも持てる
- それぞれのサブクラスには、1つのスーパークラスしか許されない
6.3 継承を不適切に適用する
- サブクラス内のオーバーライドされたメソッドで
super
を送ると、そのメッセージがスーパークラスのチェーンを上って渡されて行く - これによって、サブクラスは必要でない振る舞いを継承してしまう場合がある
6.4 抽象を見つける
- 継承が効果を発揮するために、以下の2つが常に成立している必要がある
- 「一般 - 特殊」の関係をしっかりと持っている
- 正しいコーディングテクニックを使っている
- オブジェクト指向プログラミング言語の中には、明示的にクラスを抽象概念として宣言する構文を持つものもある
- 例えば、Javeの
abstract
キーワード - Rubyは他者を信頼する性質からそのようなキーワードは備えておらず、制約を加えることはない
- 例えば、Javeの
- サブクラスをたった1つだけ持つ抽象的なスーパークラスを作ることは、無意味
- 抽象的な振る舞いを昇格する
- 具象から抽象を分ける
- テンプレートメソッドパターンを使う
- スーパークラス内で基本構造を定義して、サブクラス固有の貢献を得るためにメッセージを送るテクニック
6.5 スーパークラスとサブクラス間の結合度を管理する
- フックメッセージを使って、サブクラスを疎結合にする
-
super
を送るよう求めるのではなく、スーパークラスが代わりに「フック」メッセージを送るようにする - フックメッセージは、サブクラスがそれに合致するメソッドを実装することによって情報提供できるようにするための、専門メソッド
-
第7章 モジュールでロールの振る舞いを共有する
7.1 ロールを理解する
- 元々関連の無かったオブジェクトに、共通の振る舞いを持たせる
- この共通の振る舞いが「ロール(役割)」
- 多くのオブジェクト指向言語には、名前を付けてメソッドのグループを定義する方法が備わっている
- Rubyでは、これをモジュールと呼ぶ
- モジュールは、共通のロールを担うための完璧な方法と言える
- しかし、モジュールを使うと設計はより複雑になる
- オブジェクトは自身を管理するべき
- 自身の振る舞いは自身で持つべき
- 「オブジェクトBに関心がある時、オブジェクトBを知りたいがためにオブジェクトAの知識が求められる」ということがあってはいけない
- 具体的なコードを書く
- 抽象を抽出する
- Rubyの
extend
キーワードを使うと、オブジェクト1つだけにモジュールのメソッドを追加できる - ロールの振る舞いを継承する
7.2 継承可能なコードを書く
アンチパターン
- オブジェクトが
type
やcategory
という変数名を使い、どんなメッセージをself
に送るか決めているパターン- このようなコードは、クラスによる継承を使うように再構成できる
- 共通のコードは抽象スーパークラスに置いて、サブクラスを使って異なる型を作る
- メッセージを受け取るオブジェクトのクラスを確認してから、どのメッセージを送るかをオブジェクトが決めているパターン
- これは、ダックタイプを見落としてしまっている
- 受け手のオブジェクトは、ダックタイプのインターフェースを実装するべき
抽象に固執する
- 抽象スーパークラス内のコードを使わないサブクラスがあってはいけない
契約を守る
- サブクラスはスーパークラスと置換できることを約束する
- リスコフの置換原則(LSP)
第8章 コンポジションでオブジェクトを組み合わせる
- コンポジションとは、組み合わされた全体が、単なる部品の集合以上となるように、個別の部品を複雑な全体へと組み合わせる(コンポーズする)行為
8.1 自転車をパーツからコンポーズする
- オブジェクトは部品の集合(parts)を表すのであり、単一の部品(part)を表すのではない
- 全てのBicycleがPartsオブジェクトを必要とする
- 「Bicycle have a Parts」である
8.2 Partsオブジェクトをコンポーズする
Partをつくる
- BicycleはPartsオブジェクトを1つ持ち、Partsは複数のPartオブジェクトを持つ
Partsオブジェクトをもっと配列のようにする
- 走査と検索のための共通メソッドを使えるようにするために、
Enumerable
をインクルードする
8.3 Partsを製造する
- 他のオブジェクトを製造するオブジェクトファクトリーを使う
- Rubyの
OpenStruct
クラスを導入する
8.4 コンポーズされたBicycle
集約 - 特殊なコンポジション
- コンポジション
- 「has-a」関係を持ち、かつ包含される側のオブジェクトが包含する側のオブジェクトから独立して存在し得ない
- 集約
- コンポジションに似ているが、包含される側のオブジェクトの存在が独立していることが異なる
8.5 コンポジションと継承の選択
- 直面した問題がコンポジションで解決できるものあれば、コンポジションで解決することを優先する
- コンポジションが持つ依存は、継承が持つ依存よりはるかに少ないため
- 継承を選ぶ場合は、継承が低いリスクで高い利益を生み出してくれるとき
- 継承の利点
- **オープン・クローズド(Open-Closed)**である
- →拡張には開いており(open)、修正には閉じている(closed)
- 継承のコスト
- 継承が適さない問題に対して、誤って継承を選択
- 他のプログラマーによって、全く予期していなかった目的のために使われる可能性がある
- コンポジションの利点
- 責任が単純明快であり、明確に定義されたインターフェースを介してアクセス可能
- 適切にコンポーズされたオブジェクトは利用性が高く、想定していなかった新たなコンテキストでも簡単に利用できる
- コンポジションのコスト
- 多くのパーツに依存する
- 明示的にどのメッセージを誰に委譲するかを必ず知っておく必要がある
- is-a関係を継承に使う
- behaves-like-a関係にダックタイプを使う
- has-a関係にコンポジションを使う
第9章 費用対効果の高いテストを設計する
- 変更可能なコードを書くために欠かせない3つのスキル
- オブジェクト指向設計の理解
- コードのリファクタリングに長けていること
- 価値の高いテストを書く能力
9.1 意図を持ったテスト
テストの意図
- バグを見つける
- 仕様書となる
- 設計の決定を遅らせる
- 抽象を支える
- 設計の欠陥を明らかにする
何をテストするかを知る
- パブリックインターフェースが安定している限り、テストを一度書けば、書いた人は永遠に安心できる
テストの方法を知る
- Rubyにおいてテストのためのメインストリームのフレームワークは、
MiniTest
とRSpec
がある - テスティングの様式には、
テスト駆動開発(TDD)
と振る舞い駆動開発(BDD)
がある
9.2 受診メッセージをテストする
- 使われていないインターフェースを削除する
- パブリックインターフェースを証明する
- テスト対象のオブジェクトを隔離する
- クラスを使って依存オブジェクトを注入する
- ロールとして依存オブジェクトを注入する
9.3 プライベートメソッドをテストする
- テスト中ではプライベートメソッドは無視
- プライベートメソッドはパブリックメソッドのテストで実行されている
- プライベートメソッドは不安定
- プライベートメソッドのテストをすることで、他のメソッドがそれらを間違って使ってしまう
- テスト対象からプライベートメソッドを取り除く
- プライベートメソッド自体を作らない
- プライベートメソッドを大量に持つオブジェクトは、責任を大量に持ちすぎた可能性が高い
9.4 送信メッセージをテストする
クエリメッセージを無視する
- テストでは、selfに送られたメッセージは無視されるべき
- 外に出ていくクエリメッセージも、無視されるべき
コマンドメッセージを証明する
- メッセージを送ったことを証明する
-
モックを使う
- 状態のテストとは対照的に、モックは振る舞いのテストである
- メッセージが何を戻すかの表明をするのではなく、メッセージが送られるという期待を定義する
9.5 ダックタイプをテストする
- ロールの担い手が共有できるテスト
ロールをテストする
- テストを一度だけ書けば、ロールを担う全てのオブジェクトを再利用できる
ロールテストを使ったダブルのバリデーション
- ロールのテストを、スタブすることで生じる壊れやすさを減らすために使う
- ダックタイプのテストをしたいという要望によって、ロールに対する共有可能なテストの必要性が生まれた
- そして、一度このロールに基づいた視点を獲得してしまえば、様々な状況でそれを活用できる
9.6 継承されたコードをテストする
- 継承されたコードのテストの目標は、リスコフの置換原則を守っているか
- サブクラスの責任を規定する
- サブクラスの振る舞いを確認する
- スーパークラスによる制約を確認する
- 固有の振る舞いをテストする
- 具象サブクラスの振る舞いをテストする
- 抽象スーパークラスの振る舞いをテストする