テーマ
- 依存関係の少ない(疎結合な、変更に強い)コードを書く
- クラスは自身よりも変更の可能性の低いクラスのみ依存するべき
抑えておきたいこと
具象クラスは、抽象クラスよりも変わる可能性が高い
.
この概念については、「依存オブジェクトの注入」で一度取り上げました。そこでGearが依存していたのは、WheelとWheel.new、そしてWheel.new(rim, tire)でした。極端に具象的なコードに依存していたと言えるでしょう。しかし、コードを変更したあと、つまり、WheelがGearに注入されるようになったあとではどうでしょうか。Gearはとたんに、何かもっと抽象的なものに依存するようになりました。dimeterメッセージに応答できるオブジェクトにアクセスするようになったという事実がそれです。 Rubyに親しんでいると、このような遷移は、当然のように思えるかもしれません。しかし、少し立ち止まって考えてみましょう。同じ対策を、静的型付言語で実現するとすれば、何が必要になったでしょうか。静的型付言語はコンパイラを持ち、そのコンパイラは型に対するユニットテストのような役割を果たします。そのため、単に適当なオブジェクトをGearに注入するわけにはいきません。代わりに「インターフェース」を宣言する必要があるでしょう。diameterをインターフェースの一部として定義し、インターフェースをWheelクラスにインクルードします。その後、注入しようとしているクラスが、そのインターフェースの「一種」だとGearに教えるのです。
.
Wheel をGearへ注入することで、Gearがdeameterに応答するダックタイプに依存するように変える時、実は、さりげなくインターフェースを定義しているのです。このインターフェースは、あるカテゴリーのものはdiameterを持つ、という概念が抽象化されたものです。抽象が、具体クラスから収穫されました。
.
本質的に、抽象はより安定しています。Rubyではインターフェースを定義するために明示的な抽象を宣言する必要はありません。しかし、設計の目的のためなら、仮想的なインターフェースがクラス同様に現実に存在するものであると考えて構いません。
メモ
一応最近自分で書いたコードだと、以下部分はダックタイピング的な感じで書いたと思う(ほぼ無意識だったが。。)
header は dump
というメソッド(データを表示するふるまい)を持つオブジェクトなら任意であり、このクラスの外から注入される。
@headers.each do |header| header.dump end
コードによるメモ
以下のコードの問題点は、
- Gear の Wheel に対する依存が多い
- クラスの名前、メソッドの名前、オブジェクトの作り方、メソッドの呼び方
- Wheelの変更によって、Gearの変更が強制される可能性が高い
class Gear attr_reader :chainring, :cog, :rim, :tire def initialize(chainring, cog, rim, tire) @chainring = chainring @cog = cog @rim = rim @tire = tire end def ratio chainring / cog.to_f end def gear_inches ratio * Wheel.new(rim, tire).diameter end end class Wheel attr_reader :rim, :tire def initialize(rim, tire) @rim = rim @tire = tire end def diameter rim + (tire * 2) end end puts Gear.new(52, 11, 26, 1.5).gear_inches
- 依存オブジェクトの注入を行う
- 以下のように、Wheel インスタンスの作成をGearの外に移動する
- これにより、Gearはdiameter を実装するオブジェクトであればどれとでも共同作業ができるようになる
- Gearはdiameter メソッドへの依存1つを残すのみとなって、GearはWheelに対する知識を減らすことができた
class Gear attr_reader :chainring, :cog, :wheel def initialize(chainring, cog, wheel) @chainring = chainring @cog = cog @wheel = wheel end def ratio chainring / cog.to_f end def gear_inches # ダックタイピング # diamter メソッドを喋れるオブジェクトなら何でもOK # これを依存オブジェクトの注入 # 依存は削減され、diamter メソッドへの依存1つを残すのみとなった 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 end # Gear は diameter を知るDuckを要求する puts Gear.new(52, 11, Wheel.new(26, 1.5)).gear_inches
外部へのメッセージを隔離する
class Gear attr_reader :chainring, :cog, :wheel def initialize(chainring, cog, wheel) @chainring = chainring @cog = cog @wheel = wheel end def ratio chainring / cog.to_f end def gear_inches ratio * diameter end # 外部へのメッセージ(外部のクラスのデータやメソッドを参照する)は出来るだけ独立させる # 変更が発生しやすい箇所のため # `外部メソッドならどれでもこのように前もって隔離する対処をできるというわけではありませんが` # `それでも自分のコードを調査する価値はあるでしょう。最も脆い依存を探しだし、包み隠しましょう` def diameter 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 end # Gear は diameter を知るDuckを要求する puts Gear.new(52, 11, Wheel.new(26, 1.5)).gear_inches
参考リンク
Rubyにおける依存性の注入 - masaki's note https://scrapbox.io/masakis-note/Ruby%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B%E4%BE%9D%E5%AD%98%E6%80%A7%E3%81%AE%E6%B3%A8%E5%85%A5 →分かりやすい。 要は依存しているオブジェクトをクラスの中で生成するのではなく、期待しているふるまいを持つものを外から入れるようにすればいいと考えれば良さそう。