如果物件依賴於另一個物件,那麼當其中一個物件有所變化時另一個可能也會被迫發生變化。依賴關係會造成耦合,兩個物件耦合得越緊密,它們的行爲就越像一個單一實體,好比以前在生物課學到的共生概念,兩個物件相互依賴,就如同共生般生死與共的高度緊密關係。
Gear
會因Wheel
變化而被迫變化的狀況,可以看出Gear
對Wheel
的依賴,削弱Gear
,使它難以修改。
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_inche
當物件知道以下內容,便會形成依賴關係:
Gear
期望存在有一個名叫Wheel
的類別。Gear
期望有一個Wheel
實例回應diameter
。Gear
知道Wheel.new
需要rim
和tire
。Gear
知道Wheel.new
的第一個參數應該是rim
,第二個參數為tire
。這些依賴關係會將Gear
類別與Wheel
類別耦合在一起。換個說法,即每一個耦合都會建立依賴關係。Gear
對 Wheel
知道得細節或訊息越多,它們的耦合就越緊密。迫使原本物件很小的微調轉變成大事件,而需要大量修改,這是在開發上最不樂見的情況,開發上應盡量避免。
在軟體開發中,**「耦合」(Coupling)**是指兩個或多個模組、類別、物件等之間的相依程度或連結程度。
更具體地說,耦合表示一個模組對於其他模組的了解程度,以及兩個模組之間的交互程度。耦合程度高意味著模組之間的相互影響程度大,而耦合程度低則表示模組之間更加獨立且容易修改。
高度耦合的系統容易產生連鎖效應,一個小的變更可能需要修改大量相依的程式碼,並可能引入錯誤。低耦合度則有助於減少影響範圍,使程式碼更易於理解、修改和測試。
因此,在設計軟體系統時,通常會追求鬆散耦合的架構。
書中提及三個手法,分別是依賴注入、 隔離依賴關係、移除參數順序的依賴關係。
1.依賴注入
其意義在於知道某個類別名稱與知道傳送給該類別的訊息名稱是分屬於不同物件的責任。
Before
如果Gear
自身知道Wheel
的名稱,那麼當Wheel
的名稱發生變化時,在Gear
裡的那段程式碼也必須進行調整。更進一步,當Gear
將對Wheel
的引用寫死並深入到gear_inches
方法,那麼這便是明確地宣告它只願意為 Wheel
的實例計算齒輪英寸數,導致其他類型的物件無法使用此方法。
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
# ...
end
# Gear 期望一個知道「diameter」的「鴨子類型」
Gear.new(52, 11, 26, 1.5).gear_inches
After
將建立Wheel
新實例移動到Gear
之外就是解耦兩個類別,Gear
可以跟任何有實作diameter
物件的物件傳遞訊息,而不被Wheel
限制或影響。Gear
只需要存取一個可以回應diameter
的物件。Gear
可以不知道這個物件是否就是Wheel
類別的一個實例,只知道要保存一個會回應diameter
的物件(@wheel
) 就行。
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
# ...
end
# Gear 期望一個知道「diameter」的「鴨子類型」
Gear.new(52, 11, Wheel.new(26, 1.5)).gear_inches
2.隔離依賴關係
在條件不允許,無法解耦的情況下,將不必要的依賴關係隔離起來,避免類別受到污染,等時機成熟時再做移除。先減少 gear_inches
裡的依賴關係數量,同時公開Gear
對Wheel
的依賴。降低重複使用這段程式碼的門檻,並讓它在條件允許時更容易重構。
隔離實例建立
如果受到嚴重的束縛,導致無法藉由修改將Wheel
注入進Gear
,就在Gear
內部將建立新Wheel
實例的動作隔離起來。
1.每次建立新的Gear
時無條件地建立一個新的Wheel
。
class Gear
attr_reader :chainring, :cog, :rim, :tire
def initialize(chainring, cog, rim, tire)
@chainring = chainring
@cog = cog
@wheel = Wheel.new(rim,tire)
end
def gear_inches
ratio * wheel.diameter
end
end
2.新Wheel
實例的建立會在gear_inches
呼叫新wheel
方法時才執行。
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 wheel
@wheel ||= Wheel.new(rim, tire)
end
# ...
end
# Gear 期望一個知道「diameter」的「鴨子類型」
Gear.new(52, 11, Wheel.new(26, 1.5)).gear_inches
隔離脆弱的外部訊息
Before
假設對gear_inches
的計算需要更多的數學運算,現在wheel.diameter
被深層嵌入在一個複雜的方法裡。 gear_inches
知道wheel
有一個diameter
,這是一種很危險的依賴關係,會將gear_inches
與外部物
件以及它的一個方法耦合在一起。
def gear_inches
foo = some_intermediate_result * wheel.diameter
end
After
我們必須移除外部依賴關係,並將其封裝在某個方法裡(diameter
),試著降低修改gear_inches
的機率
def gear_inches
foo = some_intermediate_result * diameter
end
def diameter
wheel * diameter
end
在完成這項修改之後gear_inches
變得更加抽象。Gear
現在將wheel.diameter
隔離在一個單獨的方法 裡,並且gear_inches
可以依賴於某則傳送給self
的訊息。 如果Wheel
修改了diameter
方法實作的內 容,那麼對Gear
的副作用將會被限制在這個簡單的包裝方法裡。
3.移除參數順序的依賴關係
使用散列表初始化參數
為了避免Gear
建立時依賴於順序固定的參數,initialize
方法現在只接受一個參數args
,它是一個包含了所有輸入資料的散列表。Gear
現在可以自由地增加或移除初始化參數和預設值,同時也能夠放心進行任何修改。
class Gear
attr_reader :chainring, :cog, :wheel
def initialize(args)
@chainring = args[:chainring]
@cog = args[:cog]
@wheel = args[:wheel]
end
#...
end
class Wheel
attr_reader :rim, :tire
def initialize(rim, tire)
@rim = rim
@tire = tire
end
end
Gear.new(
:chainring => 52,
:wheel => Wheel.new(26, 1.5),
:cog=>11)
#<Gear:0x000000012687d378 @chainring=52, @cog=11, @wheel=#<Wheel:0x000000012687d3f0
@rim=26, @tire=1.5>>
假設即要求方法擁有多個非常穩定的參數,同時還要能選擇性允許一些不穩定的參數存在。在這種情況下,兩種技巧
都使用,即接受多個順序固定的參數,後面再緊跟一個選項散列表。
明確地定義預設值
| |
」方法| |
」方法來指定,以取得參數值(args[:chainring]
)為優先,如果參數值為nil
或false
則使用預設值(40
)。# 使用 || 來提供預設値
def initialize(args)
@chainring = args[:chainring] || 40
@cog = args[:cog] || 18
@wheel = args[:wheel]
end
fetch
方法false
或 nil
,可以使用fetch
方法來設定預設值。fetch
**方法可以更精確地處理預設值,參數不存在,則自動返回nil
。# 使用 fetch 來指定預設値
def initialize(args)
@chainring = args.fetch(:chainring, 40)
@cog = args.fetch(:cog, 18)
@wheel = args[:wheel]
end
initialize
**方法中提取到一個單獨的包裝方法。如果預設值不僅僅是一些簡單的數值或字串,最好是實作一個defaults
方法做隔離。merge
與fetch
具有相同的效果,當它們的值不在散列表裡時,這些預設值才會被合併。def initialize(args)
args = defaults.merge(args)
@chainring = args[:chainring]
# 其他參數的處理
end
def defaults
{:chainring => 40, :cog => 18}
end
封裝外部依賴:
為了避免多重依賴關係和提高代碼的可維護性,可以創建一個包裝器(wrapper)來封裝外部依賴。這個包裝器可以是一個模塊,負責使用外部類創建實例。
工廠模式:
包裝器的作用類似於工廠模式,因為它的主要目的是創建其他類的實例。工廠模式是一種設計模式,用於將對象的創建與使用分離,從而更容易維護和修改代碼。對工廠模式有興趣可以點這裡去看更詳盡的介紹。
建立Gearwrapper
模組:SomeFramework::Gear
類是app外的一部分,其初始化方法需要固定順序的參數。通過創建GearWrapper
模塊,可以將外部依賴關係隔離,並為應用程序提供更改良的接口,避免對這些參數的順序產生多重依賴關係。Gearwrapper
將所有外部介面的知識都隔離在一處,它還為你的應用程式提供了一個改良過的介面。
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
# 現在可以使用參數散列表來建立新的 Gear
p GearWrapper.gear(
:chainring => 52,
:cog=>11,
:wheel => Wheel.new(26, 1.5))
#<SomeFramework::Gear:0x00007fdd3a130050 @chainring=52, @cog=11, @wheel=#
<Wheel:0x00007fdd3a130118 @rim=26, @tire=1.5>>
這項作法的好處在於當你需要使用外部類但無法修改其參數順序時,封裝外部依賴關係並使用工廠模式是一種有效的解決方法,可以保護你的代碼免受外部依賴的影響。
參考資料