オブジェクト指向設計実践ガイド
第1章 オブジェクト指向設計
オブジェクト指向設計では、世界をオブジェクト間の自発的な相互作用の連続として捉えている。
オブジェクト指向のアプリケーションは部品から構成される。
部品が相互に作用し合い、全体の振る舞いが生まれる。
部品が「オブジェクト」であり、相互作用はオブジェクト間で受け渡される「メッセージ」である。
オブジェクト指向設計とは、「依存関係を管理すること」である。オブジェクト指向設計とは、オブジェクトが変更を許容できるような形で依存関係を構成するための、コーディングテクニックが集まったものである。設計がないと、オブジェクトがお互いを知りすぎているため、管理されていない依存関係が大混乱を引き起こす。
第2章 単一責任のクラスを設計する
「変更が簡単である」を次のように定義する
- 変更は副作用はもたらさない
- 要件の変更が小さければ、コードの変更も相応して小さい
- 既存のコードは簡単に再利用できる
- 最も簡単な変更方法はコード追加である。ただし追加するコードはそれ自体変更が容易なものとする
これに基づくと自身のコードはTRUE(見通しがよく、合理的で、利用性が高く、模範的なコード)になる。
TRUEなコードを書くための最初の一歩は、それぞれのクラスが明確に定義された単一の責任を持つように徹底することである。
次のクラスがある
class Gear
attr_reader :chainring,:cog
def initialize(chainring,cog)
@chainring = chainring
@cog = cog
end
def radio
chainring/cog.to_f
end
end
このクラスを次のように変更する
class Gear
attr_reader :chainring,:cog, :rim,:tire
def initialize(chainring,cog, rim,tire)
@chainring = chainring
@cog = cog
@rim = rim
@tire= tire
end
def radio
chainring/cog.to_f
end
def gear_inches
ratio*(rim + (tire*2))
end
end
この変更を加えたことで、従来のGear.new(chainring,cog).ratioは動作しなくなった。
例えば、このクラスに対して、「gear_inchesの値を教えて」と聞くのは理にかなっているだろうか?
また、1文でこのクラスの目的を説明できるだろうか?
もし、後者の質問に対して「または」など答えるのであれば、2つ以上の責任を負っている事になる
第3章 依存関係を管理する
オブジェクトに望まれる振る舞いは、オブジェクト自身が知っている、または継承している、もしくはそのメッセージを理解する他のオブジェクトを知っているのどれか。
一方のオブジェクトに変更を加えた時、他方のオブジェクトも変更せざるをえない恐れがあるならば、片方に依存しているオブジェクトがある。
オブジェクトが次のものを知っているとき、オブジェクトには依存関係がある
- 他のクラスの名前
- self以外のどこかに送ろうとするメッセージの名前
- メッセージが要求する引数
- それら引数の順番
重要なものは送ろうとしているメッセージであり、オブジェクトのクラスが重要である
class Gear
(省略)
def gear_inches
ratio * Wheel.new(引数).diameter
end
Gear.new(引数).gear_inches
このように書くよりも
class Gear
def gear_inches
@wheel = wheel
end
(省略)
def gear_inches
ratio * wheel.diameter
end
Gear.new(引数1,Wheel.new(引数2)).gear_inches
このように書いた方がオブジェクト間の結合は切り離される
このテクニックは「依存オブジェクトの注入」として知られている。
引数の順番への依存を取り除く
初期化の際の引数にハッシュを使う
デフォルト値を使用する
fetchメソッドを使う
複数のパラメーターを用いた初期化を隔離する
オブジェクト指向設計ではクラスの目的が、他のクラスのインスタンスの作成であることがある
このようなオブジェクトは「ファクトリー」という名前をつけている。
依存方向の選択
- 要件が変わりにくいクラスに依存する
- 抽象度が高いクラスに依存する
- 多くのところから依存されたクラスを変更すると、広範囲に影響を及ぶ
第4章 柔軟なインターフェースを作る
この章で扱う「インターフェース」とはクラス「内」にあるようなインターフェースである。
クラスに実装されているメソッドがそのクラスのパブリックインターフェースを構成する。
パブリックな部分は安定した部分であり、プライベートな部分は変化し得る部分である。
「ドメインオブジェクト」はアプリケーションにおいて、「データ」と「振る舞い」の両方を兼ね備えた「名詞」を表す。
オブジェクト間で交わされるメッセージに着目する。
メッセージの受け手がそのメッセージを担当することは相応しいのかを考える。
第5章 ダックタイピングでコストを削減する
**「ダックタイプ」**はいかなる特定のクラスとも結びつかないパブリックインターフェース
重要なことはオブジェクトが何で「ある」かではなく、何を「する」か
第6章 継承によって振る舞いを獲得する
「自分の分類を保持する属性」を獲得し、それに基づき「自身」に送るメッセージを決定する
→知識依存であり、変更のコストを上げるもの。
継承が効果を発揮するためには次の2つのことが常に成立している必要がある
①モデル化しているオブジェクトが一般-特殊の関係をしっかりと持っている
②正しいコーディングテクニックを使っている
新たな継承の階層構造へとリファクタリングをする際は、抽象を昇格できるようにコードを構成するべきであり、具象を降格するような構成にはすべきではない。
サブクラスがsuperを送るとき、サブクラスはこの知識に依存している。
スーパークラスが代わりに「フック」メッセージを送るようにすることで情報を提供できる
第7章 モジュールでロールの振る舞いを共有する
継承の手法を使うことで「ロール(役割)」を共有することができる。
第8章 コンポジションでオブジェクトを組み合わせる
コンポジション:組み合わされた全体が、単なる部品の集合以上となるように、個別の部品を複雑な全体へと組み合わせる行為。
コンポジションにおいてはより大きいオブジェクトとその部品が、「has-a」の関係によって繋げられる。
一般的なルールとして直面した問題がコンポジションで解決できるのであれば、コンポジションで解決することを優先するべきである。なぜならば、コンポジションが持つ依存は、継承が持つ依存に比べればはるかに少ないため。
継承は、その定義からして深く埋め込まれた依存の集まりを伴うもの。
コンポジションの利点
①コードは簡単に理解でき、変更が起きた場合に何が起こるかが明確
②既存の部品の亜種を追加することは「合理的」
③想定していなかった新たなコンテキストでも簡単に利用できる
コンポジションのコスト
①全体の動作が見にくい
②コードの共有の手段に乏しい
コンポジション、クラスによる継承、モジュールを使った振る舞いの共有は、コード構成のための互いに競合するテクニック
第9章 費用対効果の高いテストを設計する
テストの利益
- バグを見つける
- 仕様書となる
- 設計の決定を遅らせる
- 抽象を支える
- 設計の欠陥を明らかにする(テストを書くのが大変なのであれば、他のオブジェクトから見ても再利用が難しい)
何をテストするかを知る
受信メッセージは、その戻り値の状態がテストされるべきであり、送信コマンドメッセージは、送られたことがテストされるべき
いつテストをするかを知る
テストファーストでコードを書くことは、全体的なコストを下げる
テストの方法を知る
テストをするときは、アプリケーションのオブジェクトを大きく2つのカテゴリーに分けて考えることが役立つ。1つ目のカテゴリーは自身がテストするオブジェクト、2つ目のカテゴリーはそのほかのもの全て。
テスト中に利用可能な情報は、テスト対象オブジェクトを見ることによってのみ得ることができるべき。
- 密結合のオブジェクトのテストでは、1つのオブジェクトのテストによって、たくさんのオブジェクトのコードが実行される
テストダブル:ロールの担い手を様式化したインスタンス
スタブ:あらかじめ詰められた答えを返す
ダブルやスタブやモックといった聞きなれない言葉が出てきた
- プライベートメソッドは決して描かないこと。書くと刷れば、絶対にそれらのテストをしないこと。ただし、当然のことながら、そうすることに意味がある場合を除く
- メッセージの戻り値に対するテストを行う責任は、その受け手にある。それ以外のところでそのテストを刷れば重複になり、コストをあげてしまいます。
class Gear
def changed
observer.changed(chainring,cog)
end
この時observerのテストが、そのchangedメソッドの結果に対する責任を負う。
メッセージが送られる期待を定義するには「モック」が必要になる。