「POODR」を読んだ

Practical Object-Oriented Design in Ruby

オブジェクト指向の規則が纏まっている本。
(依存関係を管理し、インターフェースを作るためのコードの書き方についての規則)

設計に緊張が伴う理由は、これらの規則を破るためです、上手に破ることを学べば、それは設計者の一番の強みとなります。

TL;DL

ソフトウェア設計の核は

  • 作成するソフトウェアの共通部分を探し出しモジュール化する
  • 作成するソフトウェアが将来変更される部分を抽象化し変更しやすくする

1.オブジェクト指向設計

  • オブジェクト指向設計とは多態を実装する部分を決めること、依存関係の管理
    • 如何にI/Fを定義するか、実装オブジェクトを隠し抽象I/Fでやり取りをする
    • 「対応しない」責任を決めることが鍵
  • 設計の行為と実装の行為が乖離したとき、オブジェクト指向ソフトウェアは失敗する
  • 少し知識を得たころが危険。知識が増え、その見返りを求めるようになり、執拗に設計するようになってしまう
  • 不適切な場所に原則を適用し、存在しないところにパターンを見出す。手段と目的が逆になりがち

2.単一責任のクラスを設計する

  • クラスを変更する理由は複数存在してはならない
  • = 一つのクラスには複数の責任(=役割)を持たせない
  • 提供したい機能(責任、役割、関心)
  • 1つのクラスに専念するクラスは、その1つのことをアプリケーションの他の部位から隔離します
    • この隔離によって悪影響を及ぼすことのない変更と重複のない再利用を実現する

3. 依存関係を理解する

  • メソッドチェーンは依存を作り出す
  • DI, 依存の隔離, 抽象への依存
    • クラス内で別のインスタンスを作成するのはやめよう(生成は外で)
    • クラス名を知っておく責任、そのクラスに送るメソッド名を知っておく責任がどこか他のクラスに属するのでは
  • 依存方向の選択
    • “自身より変更されないものに依存しなさい”
    • 領域分け, 方向の制御

4. 柔軟なインターフェースを作る

  • 再利用困難 = 自身について外部に晒しすぎている + 近隣オブジェクトについて知りすぎている
  • 合意と約束を意識する
  • このクラスが必要なのは知ってるけどこれは何をすべきなんだろう => このメッセージを送る必要があるけど誰が応答すべきなのか
    • メッセージを送るためにオブジェクトは存在する
  • オブジェクトがそのコンテキストから完全に独立していることが求められる
  • 誰の責任なのかを明確にする、知識を隔離する
  • 「どのように」を伝えるのではなく「何を」頼むか
  • 「私は自分が何を望んでいるかを知っているし、あなたがあなたの担当分野をやってくれると信じている」
  • 明示的なI/F: 「どのように」よりも「何を」になっている
  • リファクタリング: デメテルの法則(Law of Demeter, LoD)

5. ダックタイピングでコストを削減する

  • オブジェクトが何で「ある」かではなく、何を「する」か
  • ポリモーフィズム: 多態性(多岐にわたるオブジェクトが同じメッセージに応答できる能力)
    • メッセージの送り手は受け手のクラスを気にすることなく、受け手はそれぞれが独自化した振る舞いを提供する

6. 継承によって振る舞いを獲得する

  • 継承とは根本的に"メッセージの自動委譲"の仕組み
    • 理解されなかったメッセージに対して転送経路を定義する
    • 2つのオブジェクト間の継承関係を定義するだけで転送を自動で行う
  • サブクラスはそのスーパークラスを"特化したもの"
  • 新たな継承の階層構造へとリファクタリングする際は、抽象を昇格できるよう具象を降格させないようにする
  • サブクラスがsuperを送る == そのアルゴリズムを知っている
    • p172. post_initialize のようなフックメッセージを用意するだけ。そうすることで、サブクラスは必ず super を呼ぶ責任から逃れら れます。「いつ」初期化が行われるか知らなくてよいのは大きい。結合度の低減です。

7. モジュールでロールの振る舞いを共有する

  • オブジェクトは自身を管理すべき = 自身の振る舞いは自身で持つべき StringUtil.empty(“”)
  • クラスによる継承 == である(is-a) / モジュール == のように振る舞う(behaves-like-a)
  • サブクラスはスーパクラスと置換できる(リスコフの置換原則)
  • サブクラスとスーパークラス疎結合にする(サブクラスからsuperの送信をしたくない)
  • サブクラスが自然と特化できるようにテンプレートメソッドパターンを用いる

8.コンポジションでオブジェクトを組み合わせる

  • コンポジション: 組み合わされた全体が単なる部品の集合以上となるように個別の部品を複雑な全体へと組み合わせる行為
  • 大きいオブジェクトと部品がhas-a関係になることで繋がる
  • コンポーズされたオブジェクトは"ロール"を担うオブジェクトのI/Fに依存する
  • 集約はコンポジションのようなものだが、独立したライフサイクルを持つ
    • コンポジッションは親が消えたら自身も消える、集約は消えない

9. 費用対効果の高いテストを設計する

  • テストの目的はコストの削減
  • 良い設計は自然と抽象に依存する、独立した小さなオブジェクトの集まりになる
  • テストはオブジェクトの境界に入ってくる(受信する)か、出ていく(送信する)メッセージに集中すべき
  • オブジェクトは自身のパブリックインターフェースを構成するメッセージに対して"のみ"、状態についての表明を行うべき
  • 受信メッセージはその戻り値の状態がテストされるべき
  • 送信コマンドメッセージ(副作用)は送られたことがテストされるべき
  • 送信クエリメッセージはテストするべきではない(受け手のパブリックインターフェースの一部なので、必要な状態のテストはそちらで実装しているため)
  • テストダブルとはロールの担い手を様式化したインスタンスで、テストでのみ使われる
  • 大量のプライベートメソッドを持つものは新しいオブジェクトが負う責任かもしれない
  • 状態のテストとは対照的にモックは振る舞いのテスト。メッセージが何を戻すかを表明するのではなく、メッセージが送られているという期待を定義する
    • 送信メッセージが送られたことの証明は、モックに期待を設定すれば終わり
  • 最も良いテストとは対象のコードと疎結合であり全てに対して一度だけテストをし、それが適切な場所で行われているもの(コストを上げることなく価値を追加する)

See also