この記事は「オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方」を読んだまとめです。
オブジェクト指向とは
コードの変更を容易に保つために「依存関係」を管理するテクニック。ソフトウェアは事業の要望などにより変更と追加が発生するが、行き当たりばったりに実装していると依存関係が複雑になり小さな変更でも大きな影響を与えるようになる。依存関係が複雑になると、変更するコストが高くなり、不具合も発生しやすくなるので事業の運営に悪影響を与える。オブジェクト指向は依存関係を適切にコントロールして誰でも変更しやすいソフトウェアを作るための考え方とテクニック。
単一責任のクラスを作る
一つのクラスには一つの責任(目的、用途)とするべきという考え方。おそらくメソッドも一つの責任の方がいい。一つの責任しかなければ、そのクラスに変更が発生した場合、どんな影響があるか把握しやすい。複数の責任があると変更による影響を把握しづらい。
クラスが単一責任になっているか確認するには、一文でクラスを説明してみる。「それと」「または」が含まれているようであれば責務が複数存在する可能性が高い。メソッドも複数の責任があるとメソッド名を決められなかったり長くなりがち。
単一責任は「凝縮度」とも言い換えることができる。クラスの全てがそのクラスの中心的な目的に関連していればそのクラスは凝縮度が高い。
Gearクラスの責任は「歯のある二つのスプロケット間の比を計算する」とすると以下のクラスの例のgear_inchesメソッドはタイヤの大きさまで口をだしているのでやりすぎている。「自転車へのギアの影響を計算する」ことであればgear_inchesメソッドはしっくりくるがtireを知っていることへはピンとこない。Gearクラスは二つ以上の責務を持ってしまっている。
class Gear
attr_reader:chainring,:cog,:rim,:tire
definitialize(chainring,cog,rim,tire)
@chainring=chainring
@cog=cog
@rim=rim
@tire=tire
end
def ratio
chainring/cog.to_f
end
def gear_inches
#タイヤはリムの周りを囲むので、直径を計算するためには2倍する
ratio*(rim+(tire*2))
end
end
puts Gear.new(52,11,26,1.5).gear_inches
# >137.090909090909
このクラスをすぐに改善する必要はあるのか?
「今すぐ改善」「あとで改善」の緊張感は常に存在する。コストが最小限になるように、現在の要件と未来の可能性の相互間のトレードオフを理解して決断を下すべき。
依存関係を理解する
一方のオブジェクトに変更を加えた時に、他方のオブジェクトも変更せざるを得ないおそれがあるならば、片方に依存しているオブジェクトがある。自身のアプリケーション内のクラスは、自身のアプリケーションが所有するコードにのみ依存するべき。
以下のコードはGearがWheelオブジェクトに以下の点で依存している。
- GearはWheelという名前のクラスが存在することを知っている
- GearはWheelインスタンスがdiameterに応答することを予想している
- Gearはwheel.newにrimとtireが必要なことを知っている
- GearはWheel.newの最初の引数がrimで二番目の引数がtireである必要があることを知っている。
class Gear
attr_reader:chainring,:cog,:rim,:tire
definitialize(chainring,cog,rim,tire)
@chainring=chainring
@cog=cog
@rim=rim
@tire=tire
end
def gear_inches
ratio*Wheel.new(rim,tire).diameter
end
def ratio
chainring/cog.to_f
end
class wheel
attr_reader :rim, :tire
def initialize(rim,tire)
@rim = rim
@tire = tire
end
def diameter
rim + (tire * 2)
end
end
Gear.new(52,11,26,1.5).gear_inches
end
依存オブジェクトの注入
Gearが必要としているのはdiameterを返すオブジェクトであり、Wheelではない。(ダックタイピングのオブジェクト)
GearからWheelへの参照が、gear_inchesメソッド内という深いところでハードコーディングされているとき、それは明示的に「Wheelインスタンスのギアインチしか計算する意思はない」と宣言していることにほかなりません。Gearはそれがどんな種類のものであれ、Wheel以外のオブジェクトとの共同作業を拒絶します。
diameterに応答ができるオブジェクトを渡すことで、以下の依存関係は解消される。
- GearはWheelという名前のクラスが存在することを知っている
- Gearはwheel.newにrimとtireが必要なことを知っている
- GearはWheel.newの最初の引数がrimで二番目の引数がtireである必
このテクニックを 「依存オブジェクトの注入(dependencyinjection)」という。
class Gear
attr_reader:chainring,:cog,:wheel
definitialize(chainring,cog,wheel)
@chainring=chainring
@cog=cog
@wheel = wheel
end
def gear_inches
ratio*wheel.diameter
end
制限が厳しくWhealをGearに注入できない場合はGearクラス内でWheelインスタンスの作成を隔離できないか考える。そうすることでWheelに依存しているものの、依存数は減り、依存が明確になる。
initializeメソッドでwheelインスタンスを作成する
class Gear
attr_reader:chainring,:cog,:rim,:tire
definitialize(chainring,cog,rim,tire)
@chainring=chainring
@cog=cog
@wheel = Wheel.new(rim,tire)
end
def gear_inches
ratio*wheel.diameter
end
end
wheelメソッドに隔離する
class Gear
attr_reader:chainring,:cog,:rim,:tire
definitialize(chainring,cog,rim,tire)
@chainring=chainring
@cog=cog
@rim=rim
@tire=tire
end
def gear_inches
ratio*wheel.diameter
end
def wheel
@wheel ||= wheel.new(rim,tire)
end
end
外部依存の隔離
外部へのメッセージ(メソッドの呼び出し)に注目する。外部メッセージとはself以外に送られるメッセージのこと。
class Gear
def gear_inches
ratio*wheel.diameter
end
end
ratioとwheelはselfへのメッセージだが、diameterはWheelへのメッセージとなっている。
以下のようにしてgear_inchesから依存を取り除くことができる。
class Gear
def gear_inches
ratio*diameter
end
def diameter
wheel.diameter
end
end
こうすることでWheelがdiameterの名前など変更した場合でもGearへの副作用はシンプルなラッパーメソッド(diameter)に収まるようになる。
外部への参照が埋め込まれていて、参照先が変わる可能性が高い時に参照を隔離することで変更が容易になる。
引数の順番への依存を取り除く
Gearのinitializeは引数の順番に依存している。
class Gear
attr_reader:chainring,:cog,:wheel
def initialize(chainring,cog,wheel)
@chainring=chainring
@cog=cog
@wheel = wheel
end
argsハッッシュを受け取るように変更することで引数の順番への依存から解放される。
class Gear
attr_reader:chainring,:cog,:wheel
def initialize(args)
@chainring=args[:chainring]
@cog=args[:cog]
@wheel = args[:wheel]
end
argがハッシュを受け取ることで、ハッシュキーへの依存が生まれてしまったが、引数の順番の依存と比べると軽微で、かつキーがあることで引数の役割が明確になった。
外部からしようされることが前提のフレームワークやオプションをいくつも受け取るクラスではこのテクニックを使うメリットが大きい。
もしもGearが外部のフレームワークで初期化メソッドは固定の順番を求め場合はGearを初期化するためのラッパーを作成することで依存を回避できる。この用法をファクトリーとも言う。
module GearWrapper
def self.gear(args)
SomeFrameWork::Gear.new(args[:chainring],arg[:cog],args[:wheel])
end
end