新宿(近々代々木に移転)で働く18歳エンジニアのブログ

思ったこととか、技術的なこと書きます。

(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を知れば知るほど両者間の結合はより強固になる。結合が強固になればなるほど、その二つはあたかも一つのエンティティのように振る舞うようになる。

https://gyazo.com/c1b64fb4858988d4b11909f0bb1164a0

オブジェクトが次のものを知っているとき、オブジェクトには依存関係がある。

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)ではインターフェースを定義するために明示的に抽象を宣言する必要はない。
    • 静的片付け言語では意図的に、抽象インターフェースを定義する必要がある。
  • 多くのところから依存されたクラスを変更すると広範囲に影響が及ぶ

https://gyazo.com/c37c510db42604fc44b069f3e83860a8

その他の依存関係

破壊的な類の依存

いくつものメッセージをチェーンのようにつないで遠くのオブジェクトに存在する振る舞いを実行しようとする場合 → 第4章 柔軟なインターフェースを作る

大別できる依存関係

コードに対するテストの依存関係、テストが設計を駆動する テストはコードを参照するがゆえにコードに依存する → 第9章 費用対効果の高いテストを設計する

まとめ

オブジェクトが次のものを知っているとき、オブジェクトには依存関係がある。

1.他のクラスの名前

2.self以外のどこかに送ろうとするメッセージの名前

  • 外部メッセージ(self以外に送られるメッセージ)を隔離

3.メッセージが要求する引数、引数の順番

  • 初期化の引数にハッシュを使う
  • 明示的にデフォルト値を設定する
  • デフォルト値が単純な数字や文字列以上のものである場合、initializeからデフォルト値を完全に除去し、独立したラッパーメソッド内に隔離
  • そのメソッド自体を変更することはできない場合、固定順の引数をオプションハッシュに置き換える

依存方向の選択は、「自身より変更されないものに依存するようにする」

参考リンク