オブジェクト指向設計実践ガイド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つ
3.2 疎結合なコードを書く
- 依存オブジェクトの注入
- 他のクラスに、クラス名そのもので参照してるところは、結合を生み出す主要な場所になる
- サンプルコードだと、gear_inchesメソッドで、Wheelクラスを参照してる
- この書き方だと、gear_inchesメソッドは、Wheelインスタンスのギアインチしか計算しないことになって、例えばの他にギアインチを測りたくても測れない(Wheelへの参照がハードコードされてるので)
- 本来であれば、gear_inchesメソッドは、diameterに応答できるオブジェクト(直径を測ってるくれるメソッドをもつオブジェクト)があれば良い(
Wheel.new(rim,tire)
じゃなくても良い) - Gearクラスが、他のオブジェクトについて、知りすぎてると、再利用しづらくなる
- そのため、このWheelと結合してる部分をやめて、初期化(initialize)の時に、diameterに応答できるオブジェクトを要求するように変更したら良さそう
- 修正したサンプルコード
- 他のクラスに、クラス名そのもので参照してるところは、結合を生み出す主要な場所になる
- すでにアプリケーションとして動いているコードは、いろいろと制約すでに存在している可能性があって、依存オブジェクトを注入するように変更できない場合もある。そういう場合は、クラス内で隔離するようにする。
- メッセージに着目して考える場合
- メッセージとは
- とあるオブジェクト(レシーバ)のメソッドを使うことを、メッセージを送るって表現することがある
- レシーバとメッセージという呼び方は、Smalltalkというオブジェクト指向言語でよく使われる呼び方
- 外部メッセージを隔離して、selfにメッセージを送るようにする
- 外部にメッセージを送ってるところを隔離する。隔離するっていうのは、専用のメソッド内にカプセル化するって意味
- メッセージとは
- 引数の順番への依存を取り除く
- initializeメソッドなど、初期化する時に指定されてる引数があって、順番も決まってて、という依存があるが、これらの「固定された順番の引数」への依存を簡単に回避する方法がある
- initializeメソッドに引数をただ一つargsのみを取るようにする
- 初期化の際の引数にargs(ハッシュ)が使われてるのって、引数の固定された順番への依存を解消するためだったのか。(railsとかで定義元ジャンプしていくと、引数にargsを指定されてるメソッドとかがたくさんあってよくわからんみたいな印象があったので、なるほどと思いました)
- initializeメソッドなど、初期化する時に指定されてる引数があって、順番も決まってて、という依存があるが、これらの「固定された順番の引数」への依存を簡単に回避する方法がある
- 引数の順番への依存を取り除いたサンプルコード
- 明示的にデフォルト値を設定する
||
メソッド(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など)が大きく変わることは少ない
- 具象クラスは、抽象クラスよりも変わる可能性が高い
- 多くのところから依存されたクラスを変更すると、広範囲に影響が及ぶ
- あるクラスは、他のクラスより要件が変わりやすい
- 依存の方向を考える時「自身より変更されないものに依存する」ようにした方が良い。具体的には以下の3つを考える
全体的な感想
- なかなか理解が追いつかなくて、スラスラ読めてる感じじゃないけど、読み応えがあってすごく勉強になる章だった。
- 概念的には理解できた感じあるけど、実践で具体的にコード書くとなると、まだ難しそう。ここで書いてることを意識しながら、実践でもこの考え方を使えるようにしていきたい
- ここは依存性を注入してコード書こうとか、ここは依存性を逆転させた方が良さそうとかを考えて、コードを書ける日が来るのだろうかと思ってしまったりするけど、少しづつ成長していけるよう頑張る