オブジェクト指向設計実践ガイド3章を読んだ感想

3章は依存関係を管理する話。サンプルコードを使って、オブジェクト指向設計の依存関係について説明されてて、わかりやすかった。最近は Laravelを勉強してることが多くて、その中で依存性の注入(DI)の概念とかがよく出てくるんだけど、いまいちよくわからんなーとか思ってたところを、この章の説明を読んで、理解が深まった気がする。一通り読んだ印象としては、コードの中で依存してる箇所を、どんどん取り除いていって、依存部分を別のところ定義し直して、極力依存してる箇所を減らそうよ、みたいな記述が多くあったのが特に印象に残った。それは、そうすることで再利用可能なコードになるし、複雑さが減る。依存してる部分に変更があった場合に、変更箇所が少なくて済む、などメリットがある。なんども出てきてるけど、アプリケーションを書く上で重要なのは、柔軟性のある、変更に強いコードを書くことなんだなあと思いました。以下、この章で気になったところや重要そうだと思ったところ抜粋

  • 3.1 依存関係を理解する
  • 3.2 疎結合なコードを書く
  • 3.3 依存方向の管理

3.1 依存関係を理解する

  • 依存関係があるコード例(悪い例)
  • 上記の依存関係があるコード例では、Wheelの変更によって、Gearへの変更が強制される部分が少なくても4つある(どこでしょうか?)
  • 正解は次の4つ。一定の依存関係がクラス間に築かれるのはしょうがないけど、この4つは不要な依存関係で、コードをより複雑化してしまう。
    • 他のクラスの名前
      • gear_inches メソッドの Wheel.new(rim,tire) の部分で、Gearクラスが、Wheelクラスの存在を知ってる
    • self以外のどこかに送るメッセージの名前
      • gear_inches メソッドの Wheel.new(rim,tire).diameter の部分で、Gearクラスは、Wheel クラスがdiameterというメッセージに応答することを知ってる
    • メッセージが要求する引数
      • メッセージを送る際に(gear_inches メソッドの Wheel.new(rim,tire).diameter の部分)で、Wheel.newの引数にrimとtireが必要なことを知ってる
    • そららの引数の順番
      • Wheel.new(rim,tire) の引数の順番が知られてる
  • 設計課題は「依存関係を管理して、それぞれのクラスが持つ依存を最低限にすること」です。
  • クラスが知るべきことは、自身の責任を果たすために必要十分でよい。知りすぎるのは良くない
  • 以下の図は、悪いコード例を表した図になっていて、GearがWheelを知れば知るほど、この2つのクラスの結合はより密になる。そして、あたかも1つのエンティティのように振る舞うことになってしまう
    • Wheelに変更を加えると、Gearも変更しなきゃいけなかったり
    • Gearを再利用したいだけなのに、Wheelもついて来ちゃう

  • 依存関係に関連した一般的な問題というものがある。大別して次の2つ
    • 破壊的な類の依存関係が生じる場合
      • 『「何かを知るオブジェクト」を知るオブジェクト』を知るオブジェクトがある場合
      • デメテルの法則違反
      • いくつものメソッドチェーンを繋いで、遠くのオブジェクトの振る舞い実行すること
      • 途中のオブジェクトに変更が入る可能性を考慮すべき
    • コードに対するテストの依存関係
      • テストを書き始めたプログラマーはコードと過度に結合したテストを書きがち
        • コードをリファクタリングするたびにテストが壊れる
          • テストの設計は、9章で検討する

3.2 疎結合なコードを書く

  • 依存オブジェクトの注入
    • 他のクラスに、クラス名そのもので参照してるところは、結合を生み出す主要な場所になる
      • サンプルコードだと、gear_inchesメソッドで、Wheelクラスを参照してる
      • この書き方だと、gear_inchesメソッドは、Wheelインスタンスのギアインチしか計算しないことになって、例えばの他にギアインチを測りたくても測れない(Wheelへの参照がハードコードされてるので)
      • 本来であれば、gear_inchesメソッドは、diameterに応答できるオブジェクト(直径を測ってるくれるメソッドをもつオブジェクト)があれば良い(Wheel.new(rim,tire) じゃなくても良い)
      • Gearクラスが、他のオブジェクトについて、知りすぎてると、再利用しづらくなる
      • そのため、このWheelと結合してる部分をやめて、初期化(initialize)の時に、diameterに応答できるオブジェクトを要求するように変更したら良さそう
    • 修正したサンプルコード
      • この変更をすることによってGearクラスのgear_inchesメソッドは、wheelがWheelクラスのインスタンスであるかを知ってる必要がなくて、単純にdiameterに応答するオブジェクトであるかを気にするだけでよくなった
      • 変更前は、Wheel.new(rim,tire).diameter となっていて、Wheelクラスのインスタンスであることを気にしないといけなかった
      • このような変更後のコードのことを、依存オブジェクトの注入という
  • すでにアプリケーションとして動いているコードは、いろいろと制約すでに存在している可能性があって、依存オブジェクトを注入するように変更できない場合もある。そういう場合は、クラス内で隔離するようにする。
    • gear_inchesメソッド内でWheelインスタンスが作成されてたのを、initializeメソッドで作るようにする。こうすると、gear_inchesメソッドは綺麗になって、依存はinitializeメソッドで公開される
    • wheelインスタンスを作るメソッドを別で用意する
  • メッセージに着目して考える場合
    • メッセージとは
      • とあるオブジェクト(レシーバ)のメソッドを使うことを、メッセージを送るって表現することがある
      • レシーバとメッセージという呼び方は、Smalltalkというオブジェクト指向言語でよく使われる呼び方
    • 外部メッセージを隔離して、selfにメッセージを送るようにする
      • 外部にメッセージを送ってるところを隔離する。隔離するっていうのは、専用のメソッド内にカプセル化するって意味
  • 引数の順番への依存を取り除く
    • initializeメソッドなど、初期化する時に指定されてる引数があって、順番も決まってて、という依存があるが、これらの「固定された順番の引数」への依存を簡単に回避する方法がある
      • initializeメソッドに引数をただ一つargsのみを取るようにする
      • 初期化の際の引数にargs(ハッシュ)が使われてるのって、引数の固定された順番への依存を解消するためだったのか。(railsとかで定義元ジャンプしていくと、引数にargsを指定されてるメソッドとかがたくさんあってよくわからんみたいな印象があったので、なるほどと思いました)
  • 引数の順番への依存を取り除いたサンプルコード
  • 明示的にデフォルト値を設定する
    • || メソッド(or演算子と同様の動作をする)を使う
      • || メソッドを使うと、引数に指定するargs(ハッシュ)の中で、true/falseを値にもつキーのデフォルト値がtrueの場合、明示的に値をfalseにすることができない問題がある
      • そのため、このような場合は、fetch メソッドを使うほうが良くて、fetch メソッドを使うと、フェッチしようとしてるキーの先がハッシュであることが期待されて、ハッシュのキーがない時だけ、デフォルト値を採用される。なので、|| メソッドと違って、意図的に値をfalseにできる
  • 複数のパラメーターを用いた初期化を隔離する
    • 変更が必要なメソッドを「自身で」修正できない場合に、依存関係を取り除くようにする方法についての話
    • 例えば、引数の順番の依存関係などが変更できない状況(外部のフレームワークの一部で、簡単に変更できない状況)の場合に、この依存関係を隔離する方法
    • その方法とは、外部のインターフェースを包み隠すメソッドを作る
    • 以下のサンプルコードでいうとGearWrapperモジュールの特異メソッドgearを定義して、そこでは、argsを引数に取れるようにする。そうすることで、引数の依存関係を隔離できる
    • 結果、GearWrapper.gear(:chainring => 52, :cog => 11, :wheel => Wheel.new(26, 1.5)).gear_inches の部分で、オプションハッシュを使って、Gearの新しいインスタンスを作れるようになった
  • 複数のパラメーターを用いた初期化を隔離するサンプルコード

3.3 依存方向の管理

  • 依存関係には常に方向がある
  • これまでの例は、GearがWheelやdiameterに依存していた
  • 逆に、WheelをGearやratioに依存させることもできる
  • 依存関係を逆転させたサンプルコード
  • 依存方向の選択
    • 依存の方向を考える時「自身より変更されないものに依存する」ようにした方が良い。具体的には以下の3つを考える
      • あるクラスは、他のクラスより要件が変わりやすい
        • 例えば、rubyの基本的なクラス(String,Arrayなど)が大きく変わることは少ない
      • 具象クラスは、抽象クラスよりも変わる可能性が高い
        • 依存オブジェクトの注入のところで説明した話
          • クラス内で、newしたインスタンスに依存するより、初期化時にインスタンスを必要とするように修正すると、より抽象的なものに依存するようになった
        • 静的型付言語は、インターフェースを宣言して、注入するインスタンスがそのインターフェースの一種だと、注入先のインスタンスに教える
        • rubyの場合、ダックタイプに依存していて、これは実は、さりげなくインターフェースを定義してる
        • インターフェースは、依存関係をより抽象的にするためのもの。
          • インターフェースとは、あるカテゴリーのものは、〇〇をもつ、という概念が抽象化されたもの
            • 今回のサンプルコードでいうと、wheelインスタンスは、diameterをもつ
      • 多くのところから依存されたクラスを変更すると、広範囲に影響が及ぶ

全体的な感想

  • なかなか理解が追いつかなくて、スラスラ読めてる感じじゃないけど、読み応えがあってすごく勉強になる章だった。
  • 概念的には理解できた感じあるけど、実践で具体的にコード書くとなると、まだ難しそう。ここで書いてることを意識しながら、実践でもこの考え方を使えるようにしていきたい
  • ここは依存性を注入してコード書こうとか、ここは依存性を逆転させた方が良さそうとかを考えて、コードを書ける日が来るのだろうかと思ってしまったりするけど、少しづつ成長していけるよう頑張る