【Ruby】オブジェクト思考設計実践ガイドを読み始めた(第3章)
前回に引き続き、 オブジェクト指向設計実践ガイドの読書記録です。
第3章 依存関係を整理する
先にまとめると
依存関係はなるべく少なくする
- 必要な場合は、メソッドに閉じ込めて、変更への影響範囲を最小限にする
依存する方向は、
変更が少なそうな物
に対して- 要件が明確
- 抽象クラス、インタフェース
変更が多い
かつ依存されている数が多い
クラスは存在させないようにする
依存関係って何?
そもそも依存関係って何でしょうか?
「オブジェクト指向 依存関係」と言う、ど素人感が半端ない、ワードで検索してみたところ
こちらのQiita記事を発見しました。
それが下のような物。(改行を含む場合にmdで引用を書く方法分からん。)
プログラムの要素が依存する物といえば、多くの場合は以下のような物を指す事が多いと思います。
- 参照している他の構成要素(引数や他のクラスなど)
- 使用しているライブラリやフレームワーク
前回のGearクラスの例だと、Gearクラスからオブジェクトを生成するためには、
Wheelクラスから生成されたオブジェクトが必要でした。
これが依存関係になります。
Gearの機能を全て使うためには、Wheelが必要になる
と言うことになります。
他にも実際のプログラムは多くのものに依存しています。
依存関係を作ることは、アプリケーションを作成する上で、必ず必要なものですが、
慎重に管理しないとかなり可読性、拡張性の低い物になってしまいます。
依存関係を理解する
前項で、依存関係を自分で調べた内容を記載していますが、
書籍にはこのように書かれていました。
一方のオブジェクトに変更を加えた時、他方のオブジェクトも変更せざるを得ないおそれがあるのであれば、片方に依存しているオブジェクトがあります。
書籍の通りに下のようなプログラムを考えてみましょう。
class Gear attr_reader :chainring, :cog, :data def initialize(chainring, cog, rim, tire) @chainring = chainring @cog = cog @wheel = Wheel.new(rim, tire) end def ratio chainring / cog.to_f end def gear_inches ratio * wheel.diameter end end class Wheel attr_reader :rim :tire def initialize(rim, tire) @rim = rim @tire = tire end def diameter rim + (tire * 2) end def circumference diameter * Math::PI end end @wheel = Wheel.new(10, 1.5) @gear = Gear.new(52, 11, @diameter)
このコードにおける依存関係は下の4つです。
- GearクラスはWheelと言う名前のクラスが存在することを知っている(前提に作られている)
- GearはWheelのインスタンスが
diameter
に応答することを知っている - GearはWheel.newにrimとtireが必要であることを知っている
- さらにWheel.newに必要な引数の順番についても知っている
先ほどの画像の通り、GearはWheelに依存しています。
Wheelが変更されたら、Gearも変更しなくてはならないかもしれないコードです。
依存関係自体は別に悪
と言うものではありません。
不必要な依存関係を取り除いていくことが、今回のテーマになります。
オブジェクト間の結合(CBO)
先ほどの例だと、GearがWheelのクラス・メソッドの構造を知れば知るほど、
両者の結合度は高いコードとなります。
Wheelの機能を少し変えたいだけなのに、Gearも変えないといけない
状況が発生してくると言うことです。
単体テストの視点で言うと、Gearの機能をテストするだけなのに、同時にWheelの機能のテストにもなっている状況です。
(Whellの機能を変えた場合、Gear, Whellのテストコード, Gearのテストコードを変更しないといけないと言うことになりますね。)
また、Railsなんかでは離れた関係のオブジェクトをメソッドチェーン
でつなげていくことが
あると思います。
メソッドチェーン
は明らかな依存でありかつ、チェーンの途中経路のオブジェクトどれに変更があっても、
影響が及ぶ可能性があるので、かなり危険なものとなります。
疎結合なコード
先ほどまで書いた内容を避けるための方法としては、
疎結合なコード
を書くことです。(いや、急に言われても無理じゃね?と言う意見は置いといて)
依存関係を減らすためのテクニックは存在するんです。
依存オブジェクトを注入
先ほどのGearクラスが下のような場合だったと考えます。
class Gear attr_reader :chainring, :cog, :data def initialize(chainring, cog, rim, tire) @chainring = chainring @cog = cog @rim = rim @tire = tire end def gear_inches ratio * Wheel.new(rim, tire).diameter end end
gear_inches
メソッド内で、Wheelに関する処理がハードコーディングされています。
こういったハードコーディングは、GearとWheelがガチガチに結合している物になります。
後から、別なオブジェクトのdiameter
を経由してgear_inches
を求める必要がある場合、
拡張は不可能になってしまいます。
diameter
に反応できるオブジェクトを挿入することで、
ダックタイプで反応できるようにします。
class Gear attr_reader :chainring, :cog, :data def initialize(chainring, cog, diameterable_object) @chainring = chainring @cog = cog @diameterable = diameter_object end def gear_inches ratio * diameterable.diameter end end # 外部からはdiameterメソッドを実装したオブジェクトを挿入する gear = Gear.new(10, 0.4, Wheel.new(10, 20)) puts gear.gear_inches
diameter
を知っているオブジェクト生成を外部に切り出すことで、
結合を切り離すことができます。
これは依存オブジェクトの挿入
というテクニックになります。
依存を隔離する
依存関係を取り除くことは技術的には可能かも知れませんが、
現実的に考えるとかなり難しい物になります。(特にRails使う場合なんかは、エンハンスで改善していくケースが多くなりますよね)
そういった場合は、完璧を目指さず、改善を目的とします。
依存している部分をクラス内のメソッドに閉じ込めてしまうことで、
依存している部分を最小限にすることができます。
先ほどのコードとほぼ同様ですが、下のコードの場合は考えます。
class Gear attr_reader :chainring, :cog, :data def initialize(chainring, cog, rim, tire) @chainring = chainring @cog = cog wheel = Wheel.new(rim, tire) end def gear_inches ratio * wheel.diameter end end
初期化メソッドで、wheel
オブジェクトを生成しています。
特に問題が内容に見えますが、オブジェクトの生成をメソッド経由にしてみます。
class Gear attr_reader :chainring, :cog, :data def initialize(chainring, cog, rim, tire) @chainring = chainring @cog = cog @rim = rim @tire = tire end def gear_inches ratio * wheel.diameter end private def wheel @whell ||= Wheel.new(@rim, @tire) end end
特に変化がないように見えますが、Wheel
に依存していることが、
少しわかりやすくなったように見えます。
次は、外部メッセージ
を隔離してみましょう。
外部メッセージ
とは、self以外に送られるメッセージ
を示します。
gear_inches
メソッドにおいて、wheel
の直径を出していますが、
こいつをメソッドに切り出してあげます。
def gear_inches ratio * diameter end private def wheel @whell ||= Wheel.new(@rim, @tire) end def diameter wheel.diameter end
初っ端からこういった実装が必要になるわけではないのですが、
クラスの実装をメソッドに閉じ込めていますので、依存するメソッドも切り出してあげたほうがいいと言う感じになります。
引数の順番への依存を取り除く
こういったメソッドて結構書きますよね。
def initialize(chainring, cog, rim, tire) @chainring = chainring @cog = cog wheel = Wheel.new(rim, tire) end
外部から呼び出される時に、引数の数と順番を知っている状態で呼び出すみたいなコードです。
メソッドで引数が追加されたり、削除された場合に、呼び出しもとを全て変更する必要が出てきます。
引数の受け取り方をHash
に変えることでこういったコードを改善することができます。
class Gear attr_reader :chainring, :cog, :wheel def initialize(args) @chainring = args[:chainring] @cog = args[:cog] @wheel = args[:wheel] end end gear = Gear.new( chainring: 52, cog: 11, wheel: Wheel.new(26, 11) )
コードの冗長性は増していますが、
引数の順番(index)よりも、キー名に依存させることで変更しやすいコードになりました。
依存方向の管理
先ほどコードで言う、GearがWheelに依存すべきか、
WheelがGearに依存すべきかを考えていく項目です。
依存方向を選択する場合には、変更されない物に依存する
ほうがいいです。
変更されない物に依存する
と言う言葉には、下の意味を包含しています。
- あるクラスは、他のクラスよりも要件がわかりやすい
- 要件が分かりやすいのであれば、そこまで変更される可能性は低そうですよね
- 具象クラスよりも抽象クラス
- 具象クラスは、独自メソッドも持っているので変更される可能性が高いです
- 多くの物に依存されたクラスを変更すると、アプリ全体に影響が生じる
- これは、大量に依存されたクラスはなるべく避けることが望ましいと言うことになります。
依存関係の分類
クラスを要件の変わりやすさ
とクラスに依存している物の数
の2軸で考えます。
各領域について整理すると、
- 抽象領域
- 大量のクラスに依存されているクラスはここに落ち着く
- 例えば抽象クラスとかインタフェース
- 中立領域#1
- 設計時にほとんど考慮する必要はない
- 中立領域2
- コード(要件)がとても変わりやすい
- ただ依存されている数は非常に少ないので、影響範囲は少ない
- 危険領域
- 変更のそれぞれが全てのクラスに影響する
- メンテすら難しい状態になる
通常は、抽象領域
と中立領域
に落ち着くべきです。
危険領域
に位置するクラスが存在した場合、アプリケーションのメンテはかなり
難しい物になります。
所感
概念的な物が多いので、かなり難しいです。
少しづつ業務とか個人開発に適用できるようにしていきます。