🕌

オブジェクト指向設計実践ガイドを読んで(メモ)

2023/12/23に公開

学習の一環として内容を箇条書きでまとめました。
アウトプットを前提として読むことでインプットの質も上がると思い記事を書きました。
あくまでメモ程度なので興味がある方は「オブジェクト指向設計実践ガイド」を読んでみてください。

参考 オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方

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

設計原則

  • 単一責任(SingleResponsibility)
  • オープン・クローズド(OpenClosed)
  • リスコフの置換(LiskovSubstitution)
  • インターフェース分離(InterfaceSegregation)
  • 依存性逆転(DependencyInversion)
  • デメテルの法則(LoD:LawofDemeter)

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

変更がかんたんであるとは

  • 変更は副作用をもたらさない
  • 要件の変更が小さければ、コードの変更も相応して小さい
  • 既存のコードはかんたんに再利用できる
  • 最もかんたんな変更方法はコードの追加である。ただし追加するコードはそれ自体変更が容易なものとする

変更が簡単なコードは以下の性質を持つ

  • 見通しが良い(Transparent):変更するコードにおいても、そのコードに依存する別の場所のコードにおいても、変更がもたらす影響が明白である
  • 合理的(Reasonable):どんな変更であっても、かかるコストは変更がもたらす利益にふさわしい
  • 利用性が高い(Usable):新しい環境、予期していなかった環境でも再利用できる
  • 模範的(Exemplary):コードに変更を加える人が、上記の品質を自然と保つようなコードになっている

クラスが単一責任かどうかを見極める

  • クラスの持つメソッドを質問に言い換えたときに、意味を成す質問になっているべき
    • たとえば「Gearさん、あなたの比を教えてくれませんか?」は、とても理にかなっているように思えます。しかし「Gearさん、あなたのgear_inchesを教えてくれませんか?」というのはしっくりきません。さらに「Gearさん、あなたのタイヤ(のサイズ)を教えてくれませんか?」なんていう質問は、完全にばかげています
  • 1文でクラスを説明できるか?
    • 「それと」、「または」などが必要な場合は複数の責任を負っている

設計を決定するときを見極める

  • クラスの責任の細分化と実装スピードはトレードオフ
  • どの程度クラスが発展するか、将来的な変更は予測できない

データではなく、振る舞いに依存する

  • インスタンス変数は常にアクセサメソッドで包み、直接参照しない
  • 複雑なデータ構造は隠蔽されるべきで、複雑な構造はメッセージに応答すること(振る舞い)に置き換える
    • Structを使用する
  • メソッドはクラスのように単一の責任を持つべき
  • メソッドを単一責任にすると以下の利点がある
    • クラスが行うこと全体がより明確になる
    • コメントをする必要がない
    • 再利用しやすい
    • 他のクラスへの移動が簡単
  • メソッドも1文で説明できるようにする。できない場合は単一責任ではない

クラス内の余計な責任を隔離する

  • ひとまとまりの責任を持つ振る舞いをStructを使い(Structの中にメソッドを定義することで)クラスの属性に隔離する
  • クラスを分けるときに容易になる
  • インスタンスのアトリビュート(属性)は複雑なデータ構造を持たないようにする。配列とかハッシュとか。インスタンスメソッドを作成する際、そのアトリビュートを使いたい時に、もしそのアトリビュートが複雑なデータ構造を持っていた場合、メソッド側はそのデータ構造を知らないと行けなくなる。仮にデータ構造の変更をしなければいけなくなった時にそのアトリビュートを参照しているメソッド全てを変更しなければいけなくなる。しかし、Structを使ってアトリビュートを作成するとデータ構造を振る舞いに変えることができる。「振る舞いに変える」とはそのStructを使って作成したアトリビュートが特定のメソッドに反応できることを指す。

3章 依存関係を管理する

  • 一方のオブジェクトに変更を加えたとき、他方のオブジェクトも変更せざるを得ないおそれがあるならば、片方に依存しているオブジェクトがある
  • 不必要な依存は再利用性を損なう
  • オブジェクトが次のものを知っているとき、オブジェクトには依存関係がある
    • ほかのクラスの名前
    • self以外のどこかに送ろうとするメッセージの名前
    • メッセージが要求する引数
    • それら引数の順番

疎結合なコードを書く

  • 依存オブジェクトの注入
  • クラスが知る情報を少なくすることで依存が減る
  • 外部のクラス名に対する依存をどのように管理するかは、アプリケーションに多大な影響を及ぼす
  • クラス名の依存関係が簡潔明瞭で、隔離されているアプリケーションは、新しい要求に対しかんたんに対応できる
  • self以外へメッセージを送る場合、ラッパーメソッドを作りクラス名の依存を隔離する

引数の順番への依存を取り除く

  • 初期化の際の引数にハッシュを使う
  • fetchを使ってデフォルト値を設定する
    • 引数のハッシュにキーがない場合はfetchの第二引数を初期値として設定する
    • キーがあってnilの場合はnilになる(||ではできない)
  • デフォルト値をラッパーメソッドにする
    • defaultsがマージされるのは、オプションハッシュ内に該当するキーがないときのみ
  • 外部インターフェイスの初期化のメソッドが固定順の引数を要求している場合
    • インスタンス作成のためのモジュールを作成しオプションハッシュを使う

依存方向の管理

  • 自分より変更されにくいものに依存するべき
    • 変更されにくさは 抽象 > 具象

4章 柔軟なインターフェースをつくる

パブリックインターフェイス

  • パブリックインターフェイスメソッドを定義する際に気をつけること
    • クラスの主要な責任を明らかにする
    • 外部から実行されることが想定される
    • 気まぐれに変更されない
    • 他者がそこに依存しても安全
    • テストで完全に文書化されている

プライベートインターフェイス

  • 実装の詳細に関わる
  • ほかのオブジェクトから送られてくることは想定されていない
  • 変更されやすい
  • 他者がそこに依存するのは危険
  • テストされない

パブリックインターフェイスを見つけるには

  • オブジェクトではなく、オブジェクト間で交わされるメッセージに注意を向ける
  • シークエンス図を使う
  • メッセージに注意を向けてパブリックインターフェイスを見つけるには以下を問う
    • この受け手は、このメッセージに応える責任を負うべきか、どうかを考える
    • 「このメッセージを送る必要があるけれど、だれが応答すべきなんだろう」
  • 適切に定義されたパブリックインターフェースは
    • 「どのように振る舞うか」は伝えず、「何を望むか」を伝える
      • 知識(責任)が漏れることを防ぐ
  • パブリックインターフェイスの特徴
    • 明示的にパブリックインターフェースだと特定できる
    • 「どのように」よりも、「何を」になっている(答えのみを求めて、処理の過程には関与しない)
    • 名前は、考えられる限り、変わり得ないものである
    • オプション引数として、ハッシュをとる(柔軟性を持たせる)

デメテルの法則

  • 3つ目のオブジェクトにメッセージを送る際に、異なる型の2つ目のオブジェクトを介すことを禁じる
  • 違反するとメッセージチェーン内のどこかで起こる関係のない変更によって、変更が余儀なくされるリスクが高まる

5章 ダックタイピング

  • メソッドに渡される引数が単一の目的を共に達成するために渡されてくるということを認識すること
  • 渡されてくる引数のクラスに関係なく、特定のメッセージに応答できれば良い
  • ダックタイプを適用できるパターンは以下の特徴を持つ
    • クラスで分岐するcase文
    • kind_of?とis_a?
    • responds_to?
  • 抽象性が高いのでテストの実装と文書化が必要

6章 継承

  • 継承は、共通の振る舞いを持つものの、いくつかの面においては異なるという、強く関連した型の問題を解決する
  • 継承のルール
    • モデル化しているオブジェクトが一般-特殊の関係をしっかりと持っていること
    • 新たな継承の階層構造へとリファクタリングをする際は、抽象を昇格できるようにコードを構成すべきであり、具象を降格するような構成にはすべきではない
  • テンプレートメソッドパターン
    • スーパークラス内で基本の構造を定義し、サブクラス固有の貢献を得るためにメッセージを送るというテクニック
    • スーパークラスとサブクラスに同名のメソッドを定義する。initializeでメソッドAが呼ばれば場合サブクラスのメソッドAが参照され、サブクラスにメソッドAがない場合はスーパークラスの同名のメソッドAが呼ばれる
    • 必ずinitializeでそのメソッドが必要になるにもかかわらずサブクラスにしかそのメソッドが存在しない場合、新たなサブクラスを作る際にエラーが起きる可能性が高くなる。スーパークラスにも例外を発生させる同名のメソッドを定義する。
  • サブクラスがsuperを送るとき、スーパークラスのメソッドがそのメソッド内に必要という知識を持ってしまっている。superのつけ忘れが起こる。
  • フックメッセージ
    • サブクラス固有の処理はスーパークラスのメソッド内から呼び出す。サブクラスでsuperをつけ忘れることがなくなる
    • (サブクラスにはinitailizeメソッドは作らずにスーパークラスに作成する)
    • サブクラスのインスタンスから呼ばれるのでサブクラス内でそのメソッドが探索される。(エラーが起こらないようにスーパークラスにも同名のメソッドを定義しておく)

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

  • モジュールは、さまざまなクラスのオブジェクトが、1カ所に定義されたコードを使って共通のロールを担うための方法
  • 継承とは逆でクラス間で一部の振る舞いを共有する。継承は多くの部分を共有していて一部が違うイメージ
  • メソッド探索の仕組み
    • スーパークラスとそこにインクルードされたモジュールに対して合致するメソッドを探す

継承可能なコードを書く

  • アンチパターンの特徴
    • オブジェクトがtypeやcategoryという変数名を使い、どんなメッセージをselfに送るかを決めているパターン
    • メッセージを受け取るオブジェクトのクラスを確認してから、どのメッセージを送るかをオブジェクトが決めているパターン
      • ダックタイプが使える
  • 継承について
    • サブクラスはスーパークラスのインターフェースに含まれるどのメッセージが来ても応えられるべきであり、同じ種類の入力を取り、同じ種類の出力を行う
    • 継承する側でsuperを呼び出すようなコードを書くのは避ける
  • モジュールについて
    • モジュールをインクルードするオブジェクト同士であれば、互いに入れ替えてもモジュールのロールを担える
  • テンプレートメソッドパターンについて
    • スーパークラスにメソッドAを定義して、サブクラスで同じ名前のメソッドAを定義する。テンプレートとして使われているメソッドAをオーバーライドする

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

  • コンポジションの特徴
    • コンポジションにおいては、より大きいオブジェクトとその部品が、「has-a」の関係によって繋げられる
    • コンポジションに参加するオブジェクトは小さく、構造的に独立している
    • 責任が単純明快であり、明確に定義されたインターフェースを介してアクセス可能な小さなオブジェクト
    • 同じ構造を持つ部品はFactoryパターンを使って作成ロジックを1箇所にまとめることができる
  • コンポジション、継承の使い分け
    • 一般的なルールとしては、直面した問題がコンポジションによって解決できるものであれば、まずはコンポジションで解決することを優先するべき
    • コンポジションを使う時
      • 「has-a」の関係によって繋げられる(両者に共通部分が少ない)
      • コンポーズされる側の振る舞いが、それを構成するパーツの総和を上回る場合
        • コンポーズされる側がそれ自体に多くの振る舞いを持つ場合
    • 継承を使う時
      • 過去のコードの大部分を使いつつ、新たなコードの追加が比較的少量のときに、既存のクラスに機能を追加する場合
      • 「is-a」の関係によって繋げられる(両者に共通部分が多い)
      • 共通の振る舞いを持つものの、いくつかの面においては異なるという、強く関連した型の問題
    • ダックタイプを使う時
      • いくつものさまざまなオブジェクトが共通のロールを担うことが求められるとき
      • あるオブジェクトが何かロールを担っているにもかかわらず、そのロールがそのオブジェクトの主な責任ではないとき
      • 必要性が幅広いとき。いくつもの互いに関係しないオブジェクトが、同じロールを担いたいという欲求を共有するとき

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

  • ほとんどのプログラマーはテストを書きすぎている
  • どのテストも一度だけ、それも適切な場所で行うようにする
  • テストから重複を取り除くことで、アプリケーションの変更に伴うテストの変更コストが下がる
  • テストは、オブジェクトの境界に入ってくる(受信する)か、出ていく(送信する)メッセージに集中すべき
  • 受信メッセージは、その戻り値の状態がテストされるべき
  • 送信コマンドメッセージは、送られたことがテストされるべき
  • 送信クエリメッセージは、テストするべきではない
    • 送信クエリメッセージ → 副作用がないメッセージ、値の取得等
    • 送信コマンドメッセージ → 副作用があるメッセージ、ファイルの書き込み、値の変更等
  • 基本的にプライベートメソッドはテストしない、パブリックメソッドのテストによって間接的にテストされるべき

継承されたコードをテストする

  • 階層構造に属するすべてのオブジェクトが、リスコフの置換原則を守っていることを証明すること
    • リスコフの置換原則 → サブクラスはそのスーパークラスと置換可能であるべき、すべてのサブクラスはスーパークラスのすべての振る舞いを持つべき
  • スーパークラスが持つメソッドについてのテストを書き、すべてのサブクラスでオブジェクトにそのテストをインクルードする
    • rspecではshared_examplesを使ってテスト可能
  • サブクラスが持つべき共通の振る舞い(サブクラスに定義されるメソッド群)についてのメソッドも共通化してインクルードする
  • 抽象スーパークラスの振る舞いをテストするために、スタブの為のサブクラスを作成する
    • それらのサブクラスにもサブクラスの責任のテストを適用する

Discussion