3章
アンチパターン
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
class Gear
attr_reader :chainring, :cog, :rim, :tire
def initialize(chainring, cog, rim, tire)
@chainring = chairning
@cog = cog
@rim = rim
@tire = tire
end
def ratio
chainring / cog.to_f
end
def gear_inches
# ここがやばいよ!!!
ratio * Wheel.new(rim, tire).diameter
end
end
|
Wheelへの変更によってGearへの変更が強制される。
このコードは、Gearに渡した値をWheelに渡しているので、依存性が高い。
依存関係がある とは
- ほかのクラスの名前:Gearは、Wheelという名前のクラスが存在することを予想する
- self以外のどこかに送ろうとするメッセージの名前:Gearは、Wheelのインスタンスがdiameterに応答することを予想している
- メッセージが要求する引数:Gearは、Wheel.newにrimとtireが必要なことを知っている
- それらの引数の順番:Gearは、Wheel.newの最初の引数がrimで、2番目がtireである必要があることを知っている
依存オブジェクトの注入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
class Gear
attr_reader :chainring, :cog, :wheel
# Wheelのインスタンスを引数で渡すようにする
def initialize(chainring, cog, wheel)
@chainring = chairning
@cog = cog
@wheel = wheel
def ratio
chainring / cog.to_f
end
def gear_inches
ratio * wheel.diameter
end
end
|
Gear.new(52, 11, Wheel.new(12, 1.5))
のように、引数でWheelのインスタンスを渡す。
=依存オブジェクトの注入(依存性の注入とも呼ばれる)
Gearはwheel.diameter
の部分でしかWheelとはつながっていない。しかも、知っていることは「wheel
として渡されるオブジェクトが#diameter
メソッドを持っている」ということだけ。なので、ここで渡すwheel
は#diameter
を持っているという条件を満たすだけ(返す型の問題とかはあるかもしれないけど)でいい。
依存を隔離する
インスタンス変数の作成を分離する
WheelをGearに注入できないとき。
妥協して、Gearクラス内でWheelを呼び出すメソッドとして分離する。
以下のように、wheel
メソッドに切り出して包むことで、外部クラスを呼び出していることを明示する。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
class Gear
attr_reader :chainring, :cog, :rim, :tire
def initialize(chainring, cog, rim, tire)
@chainring = chairning
@cog = cog
@rim = rim
@tire = tire
end
def ratio
chainring / cog.to_f
end
def gear_inches
ratio * wheel.diameter
end
# wheelメソッドを追加
def wheel
@wheel ||= Wheel.new(rim, tire)
end
end
|
脆い外部メッセージを隔離する
1
2
3
|
def gear_inches
ratio * wheel.diameter
end
|
ratio
はself.ratio
だけど、wheel.diameter
はwheel.diameter
である。
前述したように、wheel.diameter
という記述は「wheel
があること」「wheel
として渡されるオブジェクトが#diameter
メソッドを持っている」を前提して記述されているので、崩れやすい声質を持っている。
そのため、以下のようにカプセル化し、外部のオブジェクトが持つメソッドを直接呼び出さないようにする。
1
2
3
4
5
6
7
8
|
def gear_inches
ratio * diameter
end
def diameter
# 個人的注釈: wheelがnilだったら???
wheel.diameter
end
|
引数の順番への依存を取り除く
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
class Gear
attr_reader :chainring, :cog, :wheel
def initialize(chainring, cog, wheel)
@chainring = chainring
@cog = cog
@wheel = wheel
end
end
Gear.new(
52,
1.5,
Wheel.new(26, 1.5)
).gear_inches
|
初期化時のデフォルトの値を変えたり引数を追加したり消したりする中で、すべての実装側の引数の順番も変更することになる。
なので、ハッシュを使って以下のように書き換えられる。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
class Gear
attr_reader :chainring, :cog, :wheel
def initialize(args)
@chainring = args[:chainring]
@cog = args[:cog]
@wheel = args[:wheel]
end
end
Gear.new(
chainring: 52,
cog: 1.5,
wheel: Wheel.new(26, 1.5)
).gear_inches
|
これによって記述は冗長になったが、この冗長性は悪くない。また、順番ではなくキー名に依存することになったが、この依存はより安定している。
デフォルト値を設定する
1
2
3
4
5
|
def initialize(args)
@chainring = args[:chainring] || 40
@cog = args[:cog] || 18
@wheel = args[:wheel]
end
|
※注意
||
はfalse
もnil
と同様と捉えるので、明示的にnil
やfalse
を設定できない。
なので、例えば@bool = arg[:boolean_thing] || true
は絶対にtrueになってしまう。
……この実装はHashの[]メソッドは存在しないキー(:chainringなど)のときはnilを返すことに依存しているから。
fetch
を使用すると、この問題を解決できる。
1
2
3
4
5
|
def initialize(args)
@chainring = args.fetch(:chainring, 40)
@cog = args.fetch(:cog, 40)
@wheel = args[:wheel]
end
|
Hash#fetch (Ruby 2.7.0 リファレンスマニュアル)
デフォルト値をまとめてdefaultsメソッドに置いておく方法もある。(fetchと同じ)
1
2
3
4
5
6
7
8
9
10
|
def initialize(args)
args = defaults.merge(args)
@chainring = args[:chairing]
@cog = args[:cog]
@wheel = args[:wheel]
end
def defaults
{ chainring: 40, cog: 18 }
end
|
1
2
3
4
5
|
irb(main):020:0> d = { chainring: 40, cog: 18 }
irb(main):021:0> d.merge({ chainring: 30 })
=> {:chainring=>30, :cog=>18}
irb(main):022:0> d.merge({wheel: wheel})
=> {:chainring=>30, :cog=>18, :wheel=>#<Wheel:0x00007fb4aa945848 @rim=52, @tire=15>}
|
フレームワークなどを使っていて、それそのものを変更することができない場合は、順番の依存性を切り離すためのラップメソッドを作る。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
# Gearはフレームワークの一部なので、構造自体を変更することはできない
module SomeFramework
class Gear
def initialize(chainring, cog, wheel)
@chainring = chainring
@cog = cog
@wheel = wheel
end
end
end
# なので、ラップするためのメソッドを作る
module GearWrapper
def self.gear
SomeFramework::Gear.new(args[:chainring],
args[:cog],
args[:wheel])
end
end
GearWrapper.gear(
chainring: 52,
cog: 1.5,
wheel: Wheel.new(26, 1.5)
).gear_inches
|
モジュールであってクラスではない。
GearWrapper
の責任は、SomeFramework::Gear
のインスタンスを作成すること。
モジュールにすることで、独立した固有のオブジェクトを定義し、GearWrapperインスタンスは作成されるつもりはないという意図がある。
このような他のクラスのインスタンスを作成する目的のモジュールには、オブジェクト指向設計ではファクトリーという名前を付けている(概念の話)。
依存方向の管理
例えば、Wheelクラスの中にGearオブジェクトを使うメソッドを追加したりすると、依存方向が無茶苦茶になる。
依存方向の原則は、具体的で変わりやすいクラスは、自身より抽象的で変わりにくいクラスに依存すべし。
あるクラスの変更は、依存するすべての変更につながるため。
- あるクラスは他のクラスより要件が変わりやすい
- Rubyの基本的なクラスなど変わりにくいものに依存するコードを書くべし
- 具象クラスは抽象クラスよりも変わる可能性が高い
- より抽象的なクラスに依存するようにすべし
- ここで言う抽象というのは、個別でないみたいな感じ……。
Gearという個別のオブジェクトではなく「#diameter
メソッドを持つオブジェクト」のような
- 多くのところから依存されたクラスを変更すると、広範囲に影響が及ぶ
- 多くのところから依存されたオブジェクトは「絶対に変更されない」べし
- 覚悟の話(?)
依存されている数・要件が変わる数の二軸でグリッド分けをすると、「変更されやすく依存が多い」ゾーンがあり、そこが危険領域になる。
抽象クラスは「変更されにくいが依存が多い」ゾーンになるが、このゾーンにある=>クラスが抽象になるというわけではない(この辺の言葉遊びよくわからんけど雰囲気わかるような……)。