物件導向能有效的解決問題是因為它反應了真實世界的模型,而就如同現實中的物件,它們之間不可避免的一定會產生一些互動。
class的method可以區分成三類:
設計良好的物件應該都要遵循single responsibility,因此很自然的必須互相合作來完成複雜的問題,合作就必須知道彼此,知道彼此就會產生相依性。
相依性的定義:一個物件相依於另一個物件表示當其中一個物件改變時,另一個物件可能會被強迫也跟著改變。
class Gear
attr_reader :chainring, :cog, :rim, :tire
def initialize(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
# ...
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
一個物件有相依性,表示它知道:
以程式的講法,一個class有相依性,表示它知道:
上面的範例大部分的相依性其實都沒有必要,不必要的相依性會讓程式碼顯的不合理。因為小幅度的修改可能會被迫造成程式其它許多地方的修改,牽一髮而動全身。而這部分的設計挑戰,就是要盡可能的讓相依性減少,一個類別應該只需要知道足夠做好事情的資訊就好。
耦合 = 相依性,相依性越高 = 耦合程度就越高 = 多個物件會被視做同一個個體而很難切割或重複利用
user_phone = user.profile.contact.phone
class Gear
attr_reader :chainring, :cog, :rim, :tire
def initialize(chainring, cog, rim, tire)
@chainring = chainring
@cog = cog
@rim = rim
@tire = tire
end
def gear_inches
ratio * Wheel.new(rim, tire).diameter
end
def y_pos
Wheel.new(rim, tire).diameter / 2
end
def ratio
chainring / cog.to_f
end
end
Gear.new(52, 11, 26, 1.5).gear_inches
缺點:
改進方式:
class Gear
attr_reader :chainring, :cog, :wheel
def initialize(chainring, cog, wheel)
@chainring = chainring
@cog = cog
@wheel = wheel
end
def gear_inches
ratio * wheel.diameter
end
def y_pos
wheel.diameter / 2
end
def ratio
chainring / cog.to_f
end
end
Gear.new(52, 11, Wheel.new(26, 1.5)).gear_inches
優點:
補充:
假如很不幸的我們沒辦法更改Gear初始化的參數,一定要傳入chaining, cog, rim, tire這些值,換句話說在Gear中無法避免Wheel instance的建立,那要怎麼修改比較好?
改進方式:
class Gear
attr_reader :chainring, :cog, :rim, :tire, :wheel
def initialize(chainring, cog, rim, tire)
@chainring = chainring
@cog = cog
@wheel = Wheel.new(rim, tire)
end
def gear_inches
ratio * wheel.diameter
end
def y_pos
wheel.diameter / 2
end
def ratio
chainring / cog.to_f
end
end
Gear.new(52, 11, 26, 1.5).gear_inches
優點:
進階版本:
class Gear
attr_reader :chainring, :cog, :rim, :tire
def initialize(chainring, cog, rim, tire)
@chainring = chainring
@cog = cog
@rim = rim
@tire = tire
end
def gear_inches
ratio * wheel.diameter
end
def y_pos
wheel.diameter / 2
end
def ratio
chainring / cog.to_f
end
def wheel
@wheel ||= Wheel.new(rim, tire)
end
end
Gear.new(52, 11, 26, 1.5).gear_inches
優點:
缺點:
改進方式:
class Gear
# ...
def gear_inches
ratio * diameter
end
def y_pos
diameter / 2
end
def diameter
wheel.diameter
end
# ...
end
優點:
class Gear
attr_reader :chainring, :cog, :wheel
def initialize(chainring, cog, wheel)
@chainring = chainring
@cog = cog
@wheel = wheel
end
# ...
end
Gear.new(52, 11, Wheel.new(26, 1.5)).gear_inches
缺點:
改善方式:
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: 11,
wheel: Wheel.new(26, 1.5)).gear_inches
優點:
有三種方式:
class Gear
attr_reader :chainring, :cog, :wheel
def initialize(args)
@chainring = args[:chainring] || 40
@cog = args[:cog] || 18
@wheel = args[:wheel]
end
# ...
end
缺點:
class Gear
attr_reader :chainring, :cog, :wheel
def initialize(args)
@chainring = args.fetch(:chainring, 40)
@cog = args.fetch(:cog, 18)
@wheel = args[:wheel]
end
# ...
end
優點:
class Gear
attr_reader :chainring, :cog, :wheel
def initialize(args)
args = defaults.merge(args)
@chainring = args[:chainring]
@cog = args[:cog]
@wheel = args[:wheel]
end
# ...
def defaults
{ chaining: 40, cog: 18 }
end
# ...
end
優點:
缺點:
ArgumentError: wrong number of arguments (x for y)
。但改成hash之後,如果必要參數沒給,出錯的地方會變成在使用參數的時候出現,增加debug的困難。改善方式:
class Gear
attr_reader :chainring, :cog, :wheel
def initialize(chainring: 40, cog: 18, wheel:)
@chainring = chainring
@cog = cog
@wheel = wheel
end
# ...
end
Gear.new(
chainring: 52,
cog: 11,
wheel: Wheel.new(26, 1.5)).gear_inches
優點:
事情總是沒有想像的美好,如果Gear無法更改它的參數列(例如Gear是一個SomeFramework的class,我們無法更動它),但我們希望之後可以使用hash參數的好處,那要怎麼修改比較好?
module SomeFramework
class Gear
attr_reader :chainring, :cog, :wheel
def initialize(chainring, cog, wheel)
@chainring = chainring
@cog = cog
@wheel = wheel
end
# ...
end
end
修改方式:
module GearWrapper
def self.gear(args)
SomeFramework::Gear.new(
args[:chainring]
args[:cog]
args[:wheel])
end
end
GearWrapper.gear(
chainring: 52,
cog: 11,
wheel: Wheel.new(26, 1.5)).gear_inches
補充:
相依是具有向相性的,以上面的例子而言,改了Wheel會使Gear也跟著改,但改了Gear並不會影響Wheel,也就是Gear是相依於Wheel。我們可以重新撰寫程式碼讓相依的方向完全顛倒,下面就是一個例子。
class Gear
attr_reader :chainring, :cog
def initialize(chainring, cog)
@chainring = chainring
@cog = cog
end
def gear_inches(diameter)
ratio * diameter
end
def ratio
chainring / cog.to_f
end
# ...
end
class Wheel
attr_reader :rim, :tire, :gear
def initialize(rim, tire, chainring, cog)
@rim = rim
@tire = tire
@gear = Gear.new(chainring, cog)
end
def diameter
rim + (tire * 2)
end
def gear_inches
gear.gear_inches(diameter)
end
def y_pos
diameter / 2
end
# ...
end
Wheel.new(26, 1.5, 52, 11).gear_inches
即然我們可以改變相依性的方向,挑選相依於哪一個class就顯得特別重要。
原則:相依於一個比你還少變動的東西。
程式碼有下面三個事實:
ruby原生的class變動的程度 < framework的class變動的程度 < 你自己寫的class變動的程度
抽象性:不關聯於一個特定的instance。
一個Wheel.new所建立的instance v.s. 一個只需要respond_to diameter的@wheel變數
如果很多class都相依於某個class,則當這個class變動的時候,就是痛苦的開始。
相依程度 | 高(許多class相依於它) | A | D |
低(很少class相依於它) | B | C | |
少(不常變動) | 多(很常變動) | ||
變動頻率 |
規劃良好的程式碼,它的class會分佈在A, B, C這三個區域之中。而D則是在寫程式時應該要盡量避免。