(3) 依存関係を管理する
オブジェクトは単一の責任を持っている→複雑な問題を解決するためにはオブジェクト間で共同作業をする必要がある
一方のオブジェクトに変更を加えたとき、他方のオブジェクトも変更しなければならない=依存関係がある
class Gear def initialize(hoge, fuga, rim, tire) hogehoge end def gear_inches ratio * Wheel.new(rim, tire).diameter # 「他のクラスの名前」「引数」と「その順番」「self以外のどこかに送ろうとするメッセージの名前」の四つの依存がある。 end fugafuga end
オブジェクト間の結合(CBO: Coupling Between Objects)
GearがWheelを知れば知るほど両者間の結合はより強固になる。結合が強固になればなるほど、その二つはあたかも一つのエンティティのように振る舞うようになる。
オブジェクトが次のものを知っているとき、オブジェクトには依存関係がある。
1.他のクラスの名前
2.self以外のどこかに送ろうとするメッセージの名前
3.メッセージが要求する引数、引数の順番
一定の依存関係が二つのクラス間に築かれるのは避けられない。でも上記は不必要な依存。
1.他のクラスの名前
依存オブジェクトの注入(dependency injection)
wheelの名前に変更があったときに、Gearのgear_inchesメソッドも変更する必要がある
- GearがWheelに協力を求める必要があるとき、結局何かが、どこかでWheelクラスのインスタンスを作らなければならないから
- 名前の変更はエディターでできる
上記の理由からこの依存は無害に思える。
しかし、この依存は、明示的に「wheelインスタンスのギアインチしか計算する意思はない」と宣言していることになる。(たとえ直径を持ち合わせ、ギアを扱うオブジェクトであったとしてもwheel以外との共同作業の拒絶を意味する)
オブジェクトのクラスが重要なのではない。送ろうとしている「メッセージ」こそが重要。
Gearがアクセスする必要があるのは、diameterに応答できるオブジェクト(diameterを知っているオブジェクト)、つまりダックタイプのオブジェクト(gearはそのオブジェクトのクラスも気にせず、知るべきでもない)
class Gear def initialize(hoge, fuga, wheel) hogehoge @wheel = wheel end def gear_inches ratio * wheel.diameter end fugafuga end # gearはdiameterを知るduckを要求する Gear.new(54, 11, Wheel.new(38, 1.4)).gear_inches # Gear.new(54, 11, Bar.new(38, 1.4)).gear_inches 再利用ができる
インスタンス(Wheelインスタンス)の作成をクラス(Gear)の外に移動することで、二つのクラス間の結合が切り離される。=依存オブジェクトの注入
クラス名を知っておく責任や、そのクラスに送るメソッドの名前を知っておく責任がどこか他のクラスに属するものなのではないかとうたがえる能力がないと依存オブジェクトの注入は使えない。
依存を隔離する
- インスタンス変数の作成を分離する(dependenby injectionができない場合)
class Gear def initialize(hoge, fuga, rim, tire) hogehoge @wheel = Wheel.new(rim, tire) end fugafuga end
class Gear attr_reader :hoge, :fuga, :rim, :tire def initialize(hoge, fuga, rim, tire) hogehoge end def wheel @wheel ||= Wheel.new(rim, tire) end end
外部のクラス名に対する依存をどのように管理するか 依存するものを常に気にとめ、それらを注入することを習慣化せていけば疎結合なコードになる なんの工夫もなくその場その場でクラスを参照していくようにしない。
2.self以外のどこかに送ろうとするメッセージの名前
脆い外部メッセージ(self以外に送られるメッセージ)を隔離する
ratio, wheelはselfに送る、wheelに送っていくdiameterは外部メッセージ
gear_inchesは、wheelがdiameterに応答(diametarはwheelにメッセージを送る)することに依存している gear_inchesがwheelがdiameterを持つことを知っている
# 恐ろしい計算 foo = some_intermediate_result * wheel.diameter # 恐ろしい計算 end
gear_inchesは、selfに送るメッセージに依存するようになった。
# 恐ろしい計算 foo = some_intermediate_result * diameter # 恐ろしい計算 end def diameter wheel.diameter end
このテクニックが必要になるのは、メッセージへの参照がクラス内に埋め込まれていて、さらにそのメッセージが変わる可能性が高い時。
3.メッセージが要求する引数、引数の順番
引数が必要なメッセージをおくるとき、送り手側としては、それら引数についての知識をもたざるを得ない。
引数の順番への依存を取り除く
class Gear def initialize(hoge, fuga, wheel) @hoge = hoge @fuga = fuga @wheel = wheel end end Gear.new(25, 1.4, Wheel.new(23, 1.4)).gear_inches
class Gear def initialize(args) @hoge = args[:hoge] @fuga = args[:fuga] @wheel = args[:wheel] end end Gear.new(hoge: 25, fuga: 1.4, wheel: Wheel.new(23, 1.4)).gear_inches
メリット
- 引数の順番に対する依存性がすべて取り除かれる
- キー名が引数に関する明示的なドキュメントとなっている
デメリット
- 冗長性の増加(この冗長性には価値がある、冗長性は現時点での必要性と未来の不確実性が交差するところに存在)
- ハッシュのキー名への依存(この新しい依存は元の依存より安定していて、変更が強制されるリスクが低くなっている)
使用するかどうかはそのときによって変わる
外部から使用されることを目的としたフレームワークにおいて、かなり不安定で常用な引数をもつメソッド → 全部をハッシュ化して使った方が全体のコストが削減される可能性は高い
自分でしか使わず2つの数字を分けるだけのメソッドを書いている場合 → 単純に引数を渡して、順番への依存を拠有する方がシンプルだし、コストも安上がり
メソッドが、安定性の高い引数を数個とそれよりは安定性のないオブショナルな引数をいくつも受け取るという場合。 → 二つ組み合わせる。link_to の引数みたいな感じ
def initialize(args) @chainring = args[:chainring] || 40 @cog = args[:cog] || 18 @wheel = args[:wheel] end
# hashの[]メソッドは存在しないキーに対してはnilを返すということを頼りにしている @bool = args[:boolean_thing] || true # いずれにせよ、@boolにtrueが設定されるようになってしまっている。
したがって真偽値を引数にとったり、引数のfalseとnilの区別が必要なのであれば、デフォルト値の設定にはfetchメソッドを使う方がよい。
fetchメソッドが期待するのは、フェッチしようとしているキーがフェッチ先のハッシュにあること(なかったら、第二引数の値を返す)
Hash#fetch Returns a value from the hash for the given key. If the key can't be found, there are several options: With no other arguments, it will raise an KeyError exception; if default is given, then that will be returned; if the optional code block is specified, then that will be run and its result returned. h = { "a" => 100, "b" => 200 } h.fetch("a") #=> 100 h.fetch("z", "go fish") #=> "go fish" h.fetch("z") { |el| "go fish, #{el}"} #=> "go fish, z" The following example shows that an exception is raised if the key is not found and a default value is not supplied. h = { "a" => 100, "b" => 200 } h.fetch("z")
def initialize(args) @chainring = args.fetch(:chainring, 40) @cog = args.fetch(:cog, 18) @wheel = args[:wheel] end
デフォルト値が単純な数字や文字列以上のものである場合 initializeからデフォルト値を完全に除去し、独立したラッパーメソッド内に隔離
def initialize(args) args = defaults.merge(args) @chainring = args[:chairing] @cog = args[:cog] @wheel = args[:wheel] end def defaults {chainring: 40, cog: 18} end
依存せざるを得ないメソッドが固定順の引数を要求し、しかもそれが外部のものの場合、そのメソッド自体を変更することはできない場合。(フレームワークの一部、Gearの新しいインスタンスを自身のコード内で作成しなければならない) → 固定順の引数をオプションハッシュに置き換える
module SomeFramework class Gear attr_reader :chainring, :cog, :wheel def initialize(chainring, cog, wheel) @chainring = chainring @cog = cog @wheel = wheel end hogehoge end end module GearWrapper def self.gear(args) SomeFramework::Gear.new(args[:chainring], args[:cog], args[:wheel]) end end GearWrapper.gear( chainring: 52, cog: 11, wheel: Wheel.new(26, 1.5)).gear_inches
GearWrapperの責任は、SomeFramework::Gearのインスタンスを作成すること 目的が他のクラスのインスタンスの作成であるオブジェクト=ファクトリー
依存方向の管理
依存方向の選択は、「自身より変更されないものに依存するようにする」
- あるクラスは他のクラスより要件が変わりやすい
- 具象クラスは抽象クラスよりも変わる可能性が高い
- 抽象化されたものは、それらが共通し、安定した性質を表し、抽出元となった具象クラスより変わりにくい
- 動的型付け言語(ruby)ではインターフェースを定義するために明示的に抽象を宣言する必要はない。
- 静的片付け言語では意図的に、抽象インターフェースを定義する必要がある。
- 多くのところから依存されたクラスを変更すると広範囲に影響が及ぶ
その他の依存関係
破壊的な類の依存
いくつものメッセージをチェーンのようにつないで遠くのオブジェクトに存在する振る舞いを実行しようとする場合 → 第4章 柔軟なインターフェースを作る
大別できる依存関係
コードに対するテストの依存関係、テストが設計を駆動する テストはコードを参照するがゆえにコードに依存する → 第9章 費用対効果の高いテストを設計する
まとめ
オブジェクトが次のものを知っているとき、オブジェクトには依存関係がある。
1.他のクラスの名前
- 依存オブジェクトの注入(dependency injection)
- インスタンス変数の作成の分離
2.self以外のどこかに送ろうとするメッセージの名前
- 外部メッセージ(self以外に送られるメッセージ)を隔離
3.メッセージが要求する引数、引数の順番
- 初期化の引数にハッシュを使う
- 明示的にデフォルト値を設定する
- デフォルト値が単純な数字や文字列以上のものである場合、initializeからデフォルト値を完全に除去し、独立したラッパーメソッド内に隔離
- そのメソッド自体を変更することはできない場合、固定順の引数をオプションハッシュに置き換える
依存方向の選択は、「自身より変更されないものに依存するようにする」