LoginSignup
0

More than 3 years have passed since last update.

オブジェクト指向設計実践ガイド 2章 単一責任のクラスを設計する 簡易まとめ

Last updated at Posted at 2019-01-24

この記事の目的

オブジェクト指向設計実践ガイドを読んで学んだことを簡単にまとめる。復習したい時に全ページを読み返すのは大変なので。
初心者なので間違っている部分があるかもしれません。指摘していだだけると嬉しいです。
(この記事は、オブジェクト指向とは何かを説明するものではなく、単にオブジェクト指向設計実践ガイドの各章を簡易的にまとめたものです。)

クラスに属するものを決める

メソッドをグループ分けしてクラスにまとめる

クラスはアプリケーションの枠組みを作ることで、それ以後の工程に関わる人の考え方に制約を課す。
そのため、メソッドを正しくグループ分けしてクラスにまとめる必要がある。

変更が簡単なコードを組成する

  • 変更が簡単とは?

    • 変更は副作用をもたらさない
    • 要件の変更が小さければ、コードの変更も相応して小さい
    • 既存のコードは簡単に再利用できる
    • 最も簡単な変更方法はコードの追加である。ただし追加するコードはそれ自体変更が容易なものであるべき。
  • 「変更の簡単さ」を上記のように定義すると

    • 見通しが良い (Transparent) : 変更するコード、またはそのコードに依存する別の箇所のコードにおいても変更がもたらす影響が明白
    • 合理的 (Reasonable) : どんな変更も、かかるコストは変更がもたらす利益にふさわしい
    • 利用性が高い (Usable) : 予期していなかった環境でも使える
    • 模範的 (Exemplary) : コードに変更を加える人が自然と上記の品質を保つようなコードになっている

それぞれの頭文字を取ってTRUEと呼ぶ。TRUEなコードは変更しやすい。

単一の責任を持つクラスを作る

ここからは自転車の例を使う。以下は「ギアの比」を計算するGearクラス。

class Gear
  attr_reader :chainring, :cog

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

  def ratio
    chainring / cog.to_f
  end
end

Gear.new(52, 11).ratio #=> 4.7272727272727275

このアプリケーションを改良して、「ギアインチ」を計算したくなったとする。ギアインチの計算にはギアの比と「車輪の直径」が必要。以下がその実装。

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*(rim+(tire*2))
  end
end
Gear.new(52, 11, 26, 1.5).gear_inches #=> 137.0909090909091
Gear.new(52, 11).ratio
#=>
# wrong number of arguments (given 2, expected 4) (ArgumentError)
#   from sample.rb:20:in `new'
#   from sample.rb:20:in `<main>'

ギアインチの計算はできるようになったが、以前は動いていたGear.new(52, 11).ratioがエラーを出すようになってしまった。このコードの書き方はベストなのだろうか?

なぜ単一責任のクラスを設計するのか?

変更が簡単なアプリケーションは再利用が簡単なクラスから構成される。
責任を2つ以上持つクラスは簡単に再利用できないので、クラスは1つの責任のために作る。クラスがすることは、そのクラスの目的に強く関連する必要がある。また、再利用が簡単なクラスは着脱可能なユニットである。

クラスが単一責任かどうか見極める

上記のGearクラスは別のどこかに属するべき振る舞いまで含んでいるが、どのようにすれば見分けられるか?

  • 1つ目の方法は、クラスの持つメソッドを質問に言い換えた時に意味を成すか考える

    • Gearさん、あなたの比を教えて下さい => ok
    • Gearさん、あなたのタイヤを教えて下さい => ギアなのにタイヤ? おかしい
  • 2つ目の方法は、1文でクラスを説明してみること

    • 考えつく限り短い説明に「それと」が含まれていれば、そのクラスは2つ以上の責任を持つ可能性がある
    • 「または」が含まれていれば、そのクラスは2つ以上の責任を持ち、またそれらがあまり関係しない責任である可能性がある
  • Gearの責任を考えてみる

    • 自転車へのギアの影響を計算する
      • gear_inchメソッドはしっくりくるが、タイヤはおかしい
      • => Gearは2つ以上の責任を持っている

変更を歓迎するコードを書く

Gearにどんな変更が加わるか分からなくても、簡単に変更を受け入れることのできる構成にすることはできる。
以下は変更を歓迎するコードを書くためのテクニック

データではなく、振る舞いに依存する

単一責任のクラスでは、どんな振る舞いもただ1ヶ所に存在するようになる(DRY)。DRYなコードを書くことで、振る舞いに変更があった場合も1ヶ所のコードを変更するだけで済む。

インスタンス変数を隠す

インスタンス変数を参照する時は、直接インスタンス変数を参照するのではなく、attr_readerなどを使って参照する。

インスタンス変数を直接参照している時に、インスタンス変数を変更したくなった場合は、参照している場所全てで変更の処理を書かなくてはならないが、アクセサメソッドに隠しておけば

def value
  @value + unexpected_change
end

の様に、一箇所の変更で済む。

データ構造を隠す

複雑なデータ構造に結びつくことを避ける。

class ObscuringReferences
  attr_reader :data

  def initialize(data)
    @data = data
  end

  def diameters
    data.map { |d| d[0] + (d[1] * 2)}
  end
end
# このクラスの初期化には2次元配列が必要
# @data = [[1,2], [3,4], [5,6]]

上記のコードでは@dataの隠蔽は行われているが、これを使うためにはこのインスタンス変数の構造を知っている必要がある。diametersメソッド@dataにどのような順番で何が入っているのかを知っている。
diametersメソッドは配列の構造に依存(インデックスで参照)しているため、配列の構造が変わるとコードも変更しなくてはならない。
もしdiametersのような@dataを使うメソッドがたくさんあれば、それらのメソッドはdiameterと同じように@dataの構造を参照しなければならない。このような参照は「漏れやすい」もので、カプセル化を逃れてコード全体に広がってしまう。これはDRYではない。@dataのインデックス0に何があるかという知識は複製されるべきでなく、一箇所で把握されるべき。

では、どのようにして理解しやすい構造を作るのか?
RubyではStructクラスを使うことで構造を包み隠すことができる。

# 以下のコードはオブジェクト指向設計実践ガイドからの引用です。

class RevealingReferences
  attr_reader :wheels

  def initialize(data)
    @wheels = wheelify(data)
  end

  def diameter
    wheels.map { |wheel| wheel.rim + (wheel.tire*2) }
  end

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

このdiameter@dataの構造についての知識を持たない。知っていることはwheelsメッセージは列挙できるものを返して、それがrim, tireに応答するということだけ。以前はd[0]で参照されたものが、wheel.rimにメッセージを送ることで達成できる。

あらゆる箇所を単一責任にする

単一責任の考え方はクラス以外の部分にも役立てられる。

メソッドも単一責任にする

メソッドもクラスのように単一責任にすることで、再利用が簡単になる。
設計のテクニックも同じものを使うことができる(メソッドの役割に対しての質問、1文で責任を説明する)。

先程使った、RevealingReferencesクラスのdiameterメソッドを見てみると、このメソッドは2つ責任を持っていることがわかる。

def diameter
  wheels.map { |wheel| wheel.rim + (wheel.tire*2) }
end

wheelsを繰り返して、wheelの直径を計算している。
これを2つのメソッドに分け、それぞれを単一責任にする。

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

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

車輪の直径を1つだけ計算するメソッドを作成することが必要なのかとも思えるが、既にこれを活用できる箇所がある。
以下は以前登場した、Gearクラスのgear_inchesメソッド

def gear_inches
  ratio * (rim+(tire*2)) #直径(diameter)の計算が入っている
end

このメソッドには直径の計算が含まれているため、2つ以上の責任を持つといえるだろう。
そこで、直径の計算の部分を新しくdiameterメソッドに抽出してみる。

def gear_inches
  ratio * diameter
end

def diameter
  rim+(tire*2)
end

これでgear_inchesを単一責任にすることができる。

このようにして単一責任のメソッドを作ることで受けられる利点は以下

  • 隠された性質が明らかになる

    • クラスのリファクタリングで全てのメソッドが単一の責任を持つと、クラスが行うことの全体がわかりやすくなる。
  • コメントが不要になる

    • メソッド内のコードにコメントが必要ならば、そのコードをメソッドに抽出すれば、それがコメントの役割を果たす。
  • 再利用を促進する

    • 小さなメソッドはアプリケーションにとって健康的なコードの書き方を促進する。他のプログラマーにもコードの複製ではなく、再利用するようになる。
  • 他のクラスに移動しやすい

    • 設計に必要な情報が増えて、新しいクラスの作成を決めた時、小さなメソッドは多くのリファクタリングをしなくても簡単に移動できる。これが設計改善に対するハードルを下げる。

クラス内の責任を隔離する

Gearクラスには車輪のような振る舞いが含まれていることが分かったが、Wheelクラスを作成する必要はあるのだろうか?
設計の決定を、どうしても必要になるまで先延ばしにすることで、変更可能なコードを書くことの恩恵を受けることができる。目的は設計に手を加える数を可能な限り最小限にしつつ、Gearを単一責任にすること。
ここではWheelクラスは作らずに、Gearから車輪の振る舞いを取り除いてみる。

class Gear
  attr_reader :chainring, :cog, :rim

  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

Structにブロックを渡して、diameterを定義することでGearからWheelの振る舞いを取り除いた。
Gear内にWheelが埋め込まれているので、いつかはこれを取り除かなければならないが、Wheelに関する決定は遅らせることができた。
Gear内にWheelを埋め込むことで、WheelGearのコンテキストにおいてのみ存在すると設計者が考えていることがわかる。

Wheelクラスを作る

自転車アプリケーションに「車輪の直径」の計算を付けて欲しいという要望が出てきたとする。これはGearからWheelを独立して使いたいというニーズが出てきたということである。
StructでGear内にWheelを隔離しておいたので、Wheelクラスを作るのは簡単。

class Gear
  attr_reader :chainring, :cog, :wheel

  def initialize(chainring, cog, wheel = nil)
    @chainring = chainring
    @cog = cog
    @wheel = wheel
  end

  def ratio
    chainring / cog.to_f
  end

  def gear_inches
    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

  def circumference
    diameter * Math::PI
  end
end

wheel = Wheel.new(26, 1.5)
wheel.circumference
# 91.106186954104

Gear.new(52, 11, wheel).gear_inches
# 137.0909090909091

Gear.new(52, 11).ratio
# 4.7272727272727275

最後に

以上のように1つの責任を持つクラスを作ることで、その1つのことをアプリケーションの他の部分から隔離することができる。そうすることで、重複のない再利用と影響をもたらさない変更ができるようになる。

参考資料

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0