迷い人

日々勉強。勉強の先に何か見つかるといいなぁ

単一責任のクラスを設計する

オブジェクト指向設計実践ガイドの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