今天要來瞭解另一種介面,有別於第4章所提及的「介面」:存在於類別裡,並且由其方法所組成。另一種介面是指跨越多個類別,並獨立於單一類別的「介面」,介面代表的是一組訊息,由訊息自身定義了這個介面。
許多不同的類別作為其整體的一部分,可能是實作了介面所要求的方法,像是介面定義了某個虛擬類別,也就是說,任何類別實作了所要求的方法就可以表現得像是介面類型。
當時在五倍上課時,龍哥問了我們什麼是鴨子型別,那是我第一次聽到鴨子型別。查了許多參考資料,加上寫程式一段時間,才讓我更加理解了鴨子類型,現在透過說明,希望對正在閱讀此篇文章的你能夠有所幫助。
依據 wiki 的定義:
If it walks like a duck and it quacks like a duck, then it must be a duck.
他走起路像鴨子,具有鴨子叫聲,我們就可以說他是鴨子。
鴨子類型(Duck type)是一種用來譬喻動態型別語言(Dynamically Typed Language)的設計風格。在這種風格中,一個對象有效的語意,不是由繼承自特定的類或實現特定的介面,而是由「當前方法和屬性的集合」決定。主要根據特徵判斷,可以提供強大的靈活性,實現 多型(polymorphism) ,有關多型將會在其他章節說明,這邊先讓我們把重點放在鴨子就好。
更白話一點來說,鴨子類型指的是不會綁定到任何特定類別的公共介面,鴨子類型物件就像是變色龍,它們是按其特徵、行爲而非其類別來定義的。因此,如果物件叫起來像鴨子,走起來像鴨子,那麼其類別並不重要,它就是一隻鴨子。
物件的使用者不需要(也不應該)關心它的類別,類別只是物件取得公共介面的一種方式。物件藉由類別方式所取得的公共介面,可能只是類別所包含的公共介面中的其中一個,一個物件可以實作多個不同的介面,這稱為「鴨子類型」。正確使用鴨子類型和跨類別的類型,可以創建出靈活的、有結構的設計,其重點不在於這個物件 是什麼,而是在於它要 做什麼,這種靈活性有助於建立更容易擴展和維護的系統。
*本章延續上個章節的自行車例子繼續說明
Trip
的prepare
方法會傳送prepare_bicycles
訊息給包含在mechanic
參數裡的那個物件。在此處,儘管參數名稱為mechanic
,但它所包含的那個物件可以是任何類別。
prepare
方法對Mechanic
類別並沒有外顯的依賴關係,實際上,它依賴於所接收到可以回應prepare_bicycles
的物件。
class Trip
attr_reader :bicycles, :customers, :vehicle
#這個「mechanic」參數可以是任何類別
def prepare(mechanic)
mechanic. prepare_bicycles (bicycles)
end
#...
end
# 如果剛好傳遞了這個類別的實例,
# 它就可以運作
class Mechanic
def prepare_bicycles(bicycles)
bieyeles.each {|bicycle | prepare_bicycle(bicycle)}
end
def prepcire_bicycle (bicycle)
#...
end
end
假設需求發生了變化,除「需要技工以外,現在旅程準備還要包括一名協調員和一名司機。
# Trip的準備工作變得很複雜
class Trip
attr_reader :bicycles, :customers, :vehicle
def prepare(preparers)
preparers.each {|preparer|
case preparer
when Mechanic
preparer.prepare_bicycles(bicycles)
when Tripcoordinator
preparer.buy_food(customers)
when Driver
preparer.gas_up(vehicle)
preparer.fi1l_water_tank(vehicle)
end
}
end
end
# 當引入TripCoordinator和Driver時
class Tripcoordinator
def buy_food(customers)
# ...
end
end
class Driver
def gas__up(vehic1e)
# ...
end
def fill_water_tank(vehicle)
# ...
end
end
Trip
知道太多的具體類別和方法,這裡的每一個參數都是不同的類別,並實作了不同的方法。為了知道要傳送
哪一則訊息,你必須檢查每個參數的類別。增加case...when
敘述來選擇類別,這可以解決將正確訊息傳送到
正確物件的問題,但也會導致依賴關係暴增。
prepare
依賴於特定的類別,它依賴於那些確切的名稱。它知道每一個類別所能理解的訊息名稱,以及訊息所要
求的參數,再次違背了迪米特法則。
引入了新的訊息,Trip
的prepare
方法現在期望其參數為可以回應prepare
, trip
的Preparer
。
# Trip的準備工作變得更加簡單
class Trip
attr_reader :bicycles, :customers, :vehicle
def prepare (preparers)
preparers.each {|preparer|
preparer.prepare_trip(self) }
end
end
# 當所有準備者都是可以回應
# 「prepare_trip」的鴨子類型時
class Mechanic
def prepare_trip(trip)
trip .bicycles.each {|bicycle| prepare_bicycle(bicycle)}
end
# ...
end
class Tripcoordinator
def prepare_trip(trip)
buy_food(trip.customers)
end
# ...
end
class Driver
def prepare_trip(trip)
vehicle = trip.vehicle
gas_up(vehicle)
fill_water_tank(vehicle)
end
# ...
end
Preparer
還沒有具體的存在。它是概念上關於公共介面的一種抽象以及協定。這是一種虛構化的設計。實作prepare_trip
的物件就是Preparer
。與之相反,那些與Preparer
進行互動的物件只需要相信它們實作 Preparer
介面即可。
修改方向就會變成:prepare
方法現在期望其參數為Preparer
,並且每個參數的類別都實作了這個新介面。 prepare
方法現在可以接受新的Preparer
,同時不用被迫進行修改。如果有需要,也可以輕易地建立出其他
更多的Preparer
。
抽象程式碼最初可能很模糊,但一旦理解之後,修改會變得更加容易。使用鴨子類型會將程式碼從具體逐漸轉變為
抽象,進而讓程式碼更易於擴展。
選擇類別的case...when
敘述
有一條case...when
敘述,它用於對應用程式裡的領域物件類別名進行選擇。
class Trip
attr_reader :bicycles, :customers, :vehicle
def prepare(preparers)
preparers.each {|preparer|
case preparer
when Mechanic
preparer.prepare_bicycles(bicycles)
when Tripcoordinator
preparer.buy_food(customers)
when Driver
preparer.gas_up(vehicle)
preparer.fi1l_water_tank(vehicle)
end
}
end
end
「 kind_of?
」 和 「 is_a?
」
「kind_of?
」和「 is_a?
」(兩者其實是相同的),訊息也可以對類別進行檢查,相關的用法可以再翻閱文 件。
if preparer. kind_of? (Mechanic)
preparer.prepare_bicycles(bicycle)
elsif preparer.kind_of?(Tripcoordinator)
preparer.buy_food(customers)
elsif preparer.kind_of?(Driver)
preparer.gas_up(vehicle)
preparer.fi1l_water_tank(vehicle)
end
「responds_to?
」
對於鴨子類別尚未理解,用以取代「 kind_of?
」 和 「 is_a?
」,相關的用法可以再翻閱文件。
if preparer. responds_to? (Mechanic)
preparer.prepare_bicycles(bicycle)
elsif preparer.responds_to?(Tripcoordinator)
preparer.buy_food(customers)
elsif preparer.responds_to?(Driver)
preparer.gas_up(vehicle)
preparer.fi1l_water_tank(vehicle)
end
儘管上述做法稍微減少了依賴關係的數量,但仍有許多依賴關係存在。
鴨子類型是指那些不依賴於特定類別的物件,但能夠實作相同的公共介面。建立彼此之間互相信任的物件,專注於物件之間的公共介面,而不是具體的類別實作。物件應該只依賴於其它物件的公共介面,而非知道它們的具體類別。
建立鴨子類型時,必須要做好記錄並且測試它們的公共介面,最好的做法是好好撰寫測試,詳細的說明在第9章裡會有更多關於鴨子類型測試的內容。
Mechanic
、Driver
和Tripcoordinator
它們每一個都實作了prepare_trip
方法,這個方法是它們唯一的共同點。它們只共用了介面,而未共用具體的實作,在實作那些類別的時候,經常需要共用某些共同的行為。如何撰寫共用程式碼在第7章裡會詳細說明的內容。
儘管之前的指導原則強調不應該基於物件的具體類別來傳送訊息,但有些程式碼在實際應用中仍然使用這種方式。
舉例:
# <tt>find( :first, *args) </ttz>的便捷包装器。
# 這個方法能夠接收所有傳送給<tt>find(:first)</tt>
# 的相同參數。
def first(*args)
if args.any?
if args.first.kind_of?(Integer) ||
(loaded? && !args.first.kind_of?(Hash))
to_a.first(*args)
else
apply_finder_options(args.first).first
end
else
find_first
end
end
程式碼依賴於較為穩定和不容易改變的Ruby核心類別(例如Integer
和Hash
),在這種情況下,依賴於特定類別的實作可能是較為合理的,因為這樣的依賴相對安全。
上面這項範例的底層鴨子類型跨越了integer
和Hash
,因此在實作它時需要修改Ruby的基本類別。這種做法被稱為 「猴子補丁」(monkey patching),所謂猴子補丁是一種編程技巧,用拼湊程式碼的方法修改程式邏輯,可以在程式運行時動態修改或擴充程式碼。這樣的做法需要謹慎,因為它可能會對程式碼的穩定性和可維護性產生影響。
設計的目的是降低成本(請將這個衡量標準應用到所有狀況),如果建立鴨子類型會減少不穩定的依賴關係數量,就應該要這麼做。
今天一樣先瞭解到這,大家消化吸收一下,明天我們將針對 「型別」 去探討和延伸哦~
參考資料: