単一責任のクラスを設計する
オブジェクト指向設計実践ガイドの2章について、自分なりの理解を記述します。
理解度50%くらいで書いているので、今後加筆修正する可能性大です。
まず、自転車の歯数の比(raito)とギアインチ(gear_inches)を計算するギアクラスを考えます。
各パラメータは以下のように計算できるとします。
歯数の比(raito) = 歯数(chainring) × コグ(cog)
ギアインチ(gear_inches) = 歯数の比(raito) × 車輪の直径(diameter)
車輪の直径(diameter) = リムの直径(rim) + タイヤの厚みの2倍(tire × 2)
何も考えずにクラス実装するとおそらく以下のようになるでしょう。
class Gear def initialize(chainring, cog, rim, tire) @chainring = chainring @cog = cog @rim = rim @tire = tire end def raito @chainring / @cog.to_f end def gear_inches raito * (@rim + (@tire * 2)) end end puts Gear.new(52, 11, 26, 1.5).gear_inches
上記コードを4回に分けてオブジェクト指向設計的なコードにしていきます。
変化に強いコードにする
まず変化に強いコードにするためにインスタンス変数(@chainring等)をラッパーメソッドで包み隠します。
理由は、インスタンス変数に変更が会った時に、インスタンス変数を使ったメソッド全てを変更する必要が出てくるからです。
ランパーメソッドで包み隠していれば、ラッパーメソッドのみ修正すれば、その変更内容が全てのメソッドに反映できるからです。
attr_readerを使って書き直すと以下のようになります。
class Gear attr_reader :chainring, :cog, :rim, :tire def initialize(chainring, cog, rim, tire) @chainring = chainring @cog = cog @rim = rim @tire = tire end def raito chainring / cog.to_f end def gear_inches raito * (rim + (tire * 2)) end end puts Gear.new(52, 11, 26, 1.5).gear_inches
少し補足するとattr_readerを使うことで以下のように書いたことと同じになります。
def cog @cog end
そうすることでインスタンス変数@cogをメソッドcogで隠蔽できます。今後、cogを変更したいときはメソッドを1つ修正するだけで良くなります。
def cog @cog * ( すごく複雑な式 ) end
メソッドの責任を単一にする
次に各メソッドの責任を単一にすることを考えます。
ギアインチを計算するメソッドの中に、車輪の直径を計算する責任が組み込まれているため、この部分を分離します。
すると以下のようになります。
class Gear attr_reader :chainring, :cog, :rim, :tire def initialize(chainring, cog, rim, tire) @chainring = chainring @cog = cog @rim = rim @tire = tire end def raito chainring / cog.to_f end def gear_inches raito * diameter end def diameter rim + (tire * 2) end end puts Gear.new(52, 11, 26, 1.5).gear_inches
クラスに責任を単一にする(構造体)
次にギアクラスの責任を単一にすることを考えます。
現状のギアクラスには3つのメソッドがあります。
- 歯数の比
- ギアインチ
- 車輪の直径
この3つのうち車輪の直径はギアクラスではなく、ホイールクラスで持つべきだと判断したとします。
ホイールクラスとして切り出す前に、ギアクラスの中で構造体(Struct)として以下のように定義します。
class Gear attr_reader :chainring, :cog, :wheel def initialize(chainring, cog, rim, tire) @chainring = chainring @cog = cog @wheel = Wheel.new(rim, tire) end def raito chainring / cog.to_f end def gear_inches raito * wheel.diameter end Wheel = Struct.new(:rim, :tire) do def diameter rim + (tire * 2) end end end puts Gear.new(52, 11, 26, 1.5).gear_inches
クラスの責任を単一にする(構造体を分離)
最後に構造体をホイールクラスとしてギアクラスから分離します。
class Gear attr_reader :chainring, :cog, :wheel def initialize(chainring, cog, wheel=nil) @chainring = chainring @cog = cog @wheel = wheel end def raito chainring / cog.to_f end def gear_inches raito * 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 @wheel = Wheel.new(26, 1.5) puts Gear.new(52, 11, @wheel).gear_inches
これで、ギアクラス、ホイールクラス共に単一の責任を持ち、かつインスタンス変数が隠蔽されて仕様変更に強いコードが出来上がりました。
参考図書
オブジェクト指向設計実践ガイド 2章
Ruby におけるハッシュ (Hash) と構造体 (Struct) の使い分け - Qiita