この記事の目的
オブジェクト指向設計実践ガイドを読んで学んだことを簡単にまとめる。復習したい時に全ページを読み返すのは大変なので。
初心者なので間違っている部分があるかもしれません。指摘していだだけると嬉しいです。
(この記事は、オブジェクト指向とは何かを説明するものではなく、単にオブジェクト指向設計実践ガイドの各章を簡易的にまとめたものです。)
クラスに属するものを決める
メソッドをグループ分けしてクラスにまとめる
クラスはアプリケーションの枠組みを作ることで、それ以後の工程に関わる人の考え方に制約を課す。
そのため、メソッドを正しくグループ分けしてクラスにまとめる必要がある。
変更が簡単なコードを組成する
-
変更が簡単とは?
- 変更は副作用をもたらさない
- 要件の変更が小さければ、コードの変更も相応して小さい
- 既存のコードは簡単に再利用できる
- 最も簡単な変更方法はコードの追加である。ただし追加するコードはそれ自体変更が容易なものであるべき。
-
「変更の簡単さ」を上記のように定義すると
- 見通しが良い (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
を埋め込むことで、Wheel
はGear
のコンテキストにおいてのみ存在すると設計者が考えていることがわかる。
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つのことをアプリケーションの他の部分から隔離することができる。そうすることで、重複のない再利用と影響をもたらさない変更ができるようになる。