オブジェクト指向設計実践ガイド Rubyで分かる柔軟なアプリケーションの育て方 第2章まとめ

おはようございます。
今日は「オブジェクト指向設計実践ガイド Rubyで分かる柔軟なアプリケーションの育て方」の第2章を自分なりにまとめていきたいと思います。

第二章 単一クラスを設計する

なぜ単一責任のクラスを設計することが重要なのか

→ これは変更が簡単にできるようなアプリケーションを組みたいからになります。2つ以上の責任を持つクラスは簡単には変更できません。

では単一責任で簡単なコードを書くとどんないいことがあるのか

・見通しがよい(transparent):変更するコードにおいても、そのコードに依存する別の場所のコードにおいても変更がもたらす影響が明白である。難しくしてますが、分かりやすいコードということですね。

・合理的(reasonable):どんな変更であっても、かかるコストは変更がもたらす利益にふさわしい。すぐに変更が可能ということですね。

・利用性が高い(Usable):新しい環境、予期していなかった環境でも再利用できる。

・模範的(Exemplary):コードに変更を加える人が、上記の品質を自然と保つようなコードになっている。

※ これらの頭文字をとってTRUEなコードという。

クラスが単一責任かどうかを見極めるためには

方法は2つあります。
① クラスの持つメソッドを質問に言い換えたときにクラスに聞くべき質問であるかを確かめる
② 一文でクラスを説明してみる。考えつく限り短い説明に「それと」や「または」が入っている場合は2つ以上の責任を負っている。
説明だけだと分かりにくいので例を見ていきます。

class Gear
  attr_render :chainring, :cog, :rim, :tire
  def initialize(chainring, cog, rim, tire)
    @chainring = chainring
    @cog = cog
    @rim = rim
    @tire = tire
  end
  
  def ratio
    chainring / cog.to_s
  end

  def gear_inches
    ratio * (rim + (tire * 2))
  end
end


この例を使って①で確かめると、「Gearさん、あなたの比を教えてくれませんか?」はとても理にかなった質問です。しかし、「Gearさん、あなたのギアインチを教えてくれませんか?」はグレー、「Gearさん、あなたのタイヤのサイズを教えてくれませんか?」は完全に的ハズレな質問なのが直感的に分かると思います。これにより、Gearクラスは2つ以上の責任を追っていることが分かります。
また、②のやり方でクラスを一文で表すと、「自動車へのギアの影響を計算する」となります。どう考えてもギアとタイヤは直接的に関係がないため、タイヤのサイズについてはこのクラスで責任を持たすべきではないことが分かります。

「変更しやすいコードを書く」というテーマで悪い例を挙げて、どこが悪いかを一つずつ見ていく

ここからは表題の通り、悪い例を挙げて指摘していきます。

例①

class Gear
  def initialize(chainring, cog)
    @chainring = chainring
    @cog = cog
  end

  def ratio
    @chainring / @cog.to_f
  end
end


この例はよろしくありません。なぜなら@cogが10箇所で参照されていたとすると、@cogを修正する必要があったときに@cogが使われている箇所すべてを修正する必要があるからです。なので、最初の段階からそうした場合に備えて以下のようにした方がよいです。

class Gear
  attr_render :chainring, :cog     # 追加
  def initialize(chainring, cog)
    @chainring = chainring
    @cog = cog
  end

  def ratio
    @chainring / @cog.to_f
  end
end


自分もすごく疑問に思ったところを補足します。手間を減らすためなら、initializeメソッドを変更すれば一箇所の修正だけで終わるじゃないかという意見があります。しかし、インスタンス変数の参照先で@cogをそのまま使いたい場合に困ります。例えばオートマ車を想像すると分かりやすいです。オートマ車では、速度によって@cogが変わるのでいろいろな@cogを定義したいです。そういった場合、インスタンス変数に直接アクセスするようなやり方では実装できないため、アクセサメソッドを用意しておいたほうが後々変更が簡単になります。


例②

class ObscuringReferences
  attr_reader :data
  def initialize(data)
    @data = data
  end

  def diameters
    data.collect {|cell|
      cell[0] + (cell[1] * 2)     # 0はリムを1はタイヤを表す。
    end
  end
    # インデックスで配列の値を参照するメソッドが他にも多数ありとする
end


この例もよろしくないです。なぜなら、diametersメソッドが配列の構造に依存しているからです。配列の構造が変われば、コードをまるごと変更しなければなりません。では、どのように書けばいいのか。それは配列で示され複雑な構造をStructクラスを使って隔離します。具体的なコードを見ていきましょう。

class ObscuringReferences
  attr_reader :wheels
  def initialize(data)
    @wheels = wheelify(data)
  end

  def diameters
    wheels.collect {|wheel|
      wheel.rim + (wheel.tire * 2)
    end
  end

  wheel = Struct.new(:rim, :tire)
  def wheelify(data)
    data.collect {|cell|
      wheel.new(cell[0], cell[1]
  end
end


まず、Structクラスとはなんなのかを簡単に書きます。

Structクラスとは

簡易的なクラスで、まとまったデータを扱いたいがクラスを作るまでもなく、クラス内で特定のデータのまとまりを表現する場合に用います。

Structクラスの特徴

① Struct.newの引数に渡したシンボルはクラスの属性になり、値を読み書きするメソッドが自動的に作成される。
② 作成したクラスのnewメソッドに渡した引数は、それぞれStruct.newで作成した属性に対応する属性値となる。


こうやってまとめてみるとめちゃくちゃ便利なやつですね。
このStructクラスを使ってリムやタイヤのデータを隔離することで、配列の構造が変わってもStructクラスの箇所を修正するだけでよくなり外部データの構造の変化に強くなりました。

「あらゆる箇所を単一責任にする」というテーマで悪い例を挙げて、どこが悪いかを一つずつ見ていく

今度はあらゆる箇所を単一責任にするという目的で見ていきます。あらゆるメソッドを単一責任とすることでクラスのスコープが明白になり、余計な責任を隔離しやすくすることができるようになります。では例を見ていきます。


例③

def diameters
  wheels.collect {|wheel|
    whell.rim + (wheel.tire * 2)
end


このメソッドは責任を2つ背負っています。wheelsに対して繰り返し処理を行うと同時に、wheelの直径を計算しています。これを2つのメソッドに分けるとこうなります。

def diameters
  wheels.collect {|wheel| diameter(whell) }
end

def diameter(whell)
  whell.rim + (wheel.tire * 2)
end


このリファクタリングのより、diameterを1つ分取得できるようになりました。これを使ってギアインチの計算もすることができるようになります。

def gear_inches
  wheels.collect {|wheel| gear_inche(wheel) }
end

def gear_inche(wheel)
  diameter(wheel) * ratio
end


メソッドを単一責任にしただけですが、たったこれだけのコードの追加でギアインチを計算できるようになるという大きな副産物をもたらしてくれました。

最初の例をリファクタリングし、単一責任のクラスをつくる

最初の例に戻ると、gear_inchesメソッドは次のように単一責任のメソッドに分けることができます。

def gear_inches
  ratio * diameter
end

def diameter
  rim + (tire * 2)
end


このようにメソッドを分けるとdiameterメソッドは車輪のような振る舞いをしており、Gearクラスから隔離するべきだということが見えてきます。それでは最初の例を変更がしやすい理想的なコードに編集します。

class Gear
  attr_render :chainring, :cog, :wheel
  def initialize(chainring, cog, rim, tire)
    @chainring = chainring
    @cog = cog
    @wheel = Wheel.new(rim, tire)
  end
  
  def ratio
    chainring / cog.to_f
  end

  def gear_inches
    ratio * wheel.diameter
  end

  Wheel = Struct.new(:rim, :tire) do
    def diameter
      rim + (tire * 2)
    end
  end
end


これでWhellを使う別のメソッドを導入する際、簡単にWheelクラスを独立させることができるようになっています。


今回はこれで終わります。ここまで読んでくださり、ありがとうございました。分かりにくいやアドバイス等ありましたらコメントくださると幸いです。では!