其幾天我們從單一個類別,像是水平擴展一樣,討論耦合與介面等問題。
而今天我們要開始討論的是垂直擴展的繼承問題~
個人覺得繼承概念其實沒有很困難,但要注意到細節!
首先是繼承的定義
繼承(英語:inheritance)是物件導向軟體技術當中的一個概念。如果一個類別B「繼承自」另一個類別A,就把這個B稱為「A的子類」,而把A稱為「B的父類別別」也可以稱「A是B的超類」。
而繼承的核心其實就是訊息自動委派
最簡單的例子就是界門綱目科屬種,每一個分類各自向下延展時,會發現有些行為是共通擁有的。
例如,哺乳類具有新皮質、毛皮、三個聽小骨和乳腺...等特性,而人類因為屬於哺乳類所以也有這些特性~
而在程式語言中,當有許多共通行為時,就可以向上提取,成為父類別。既然一個方法可以供給多個類別使用,這就是抽象層的概念。
總結來說,向上提取的父類別為抽象,就把具體留給子類別,這時候面臨一個問題!
要從具體提取抽象,還是從抽象下放具體? (提示:選擇造成危害較小的那一個)
假設從抽象下放具體,那有那麼幾個具體介面(方法)並沒有完全根除時,會造成其他類別因為沒有覆寫而讓具體行為展露,這會造成在不對的類別內顯現不對的行為,例如哺乳類別內,殘留人類的具體行為(寫程式),然後因為貓類別繼承哺乳類,卻沒有寫程式的行為,而沿著繼承到哺乳類...
(我的貓也會寫Ruby了!)
相反的,若是具體向上組成抽象層,會因為具體沒有抽離乾淨,而抽象類別缺乏行為,最大的問題頂多噴錯找不到方法,或者違反DRY原則,並不會讓類別展現錯誤的行為。
繼承的耦合,常常發生在子類別擁有控制權,這潛藏著不定性的危機
如下面範例:
class Bicycle
attr_reader :size, :chain, :tire_size
def initialize(args = {})
@size = args[:size]
@chain = args[:chain] || default_chain
@tire_size = args[:tire_size] || default_tire_size
end
def spares
{tire_size: tire_size,
chain: chain}
end
def default_chain
'10-speed'
end
def default_tire_size
raise NoImplementedError
end
end
class RecumbentBike < Bicycle
attr_reader :flag
def initialize(args = {})
@flag = args[:flag]
end
def spares
super.merge({flag: flag})
end
def default_chain
'9-speed'
end
def default_tire_size
'28'
end
end
bent = RecumbentBike.new(flag: 'tall and orange')
puts bent.spares
#{:tire_size=>nil, :chain=>nil, :flag=>"tall and orange"}
#因為子類別忘記需要部分參數來自父類別,所以出現:tire_size=>nil, :chain=>nil
class RecumbentBike < Bicycle
def initialize(args = {})
@flag = args[:flag]
super
end
#{:tire_size=>"28", :chain=>"9-speed", :flag=>"tall and orange"}
#這時候可以運作成功,但就不能忘記要加上super(更動子類別來操作父類別的參數)
end
class Bicycle
attr_reader :size, :chain, :tire_size
def initialize(args = {})
@size = args[:size]
@chain = args[:chain] || default_chain
@tire_size = args[:tire_size] || default_tire_size
post_initialize(args) #@flag = args[:flag]
end
def spares
{tire_size: tire_size,
chain: chain}.merge(local_spares) #{flag: flag}
end
def default_chain
'10-speed'
end
def default_tire_size
raise NoImplementedError
end
def post_initialize(args)
end
end
class RecumbentBike < Bicycle
attr_reader :flag
def post_initialize(args)
@flag = args[:flag]
end
def default_chain
'9-speed'
end
def default_tire_size
'28'
end
def local_spares
{flag: flag}
end
end
bent = RecumbentBike.new(flag: 'tall and orange')
puts bent.spares
#{:tire_size=>"28", :chain=>"9-speed", :flag=>"tall and orange"}
特別解釋一下Hook
運作流程:
RecumbentBike.new
時因為RecumbentBike
類別沒有initialize
所以經由繼承找到Bicycle
內Bicycle
內在initialize
時執行了post_initialize(args)
此時的(args)是flag: 'tall and orange'
但是這個方法並不是Bicycle
內的,而是RecumbentBike
所以會回傳@flag = args[:flag]
bent.spares
時會有.merge(local_spares)
的步驟,local_spares
源於RecumbentBike
{flag: flag}
merge
成{tire_size: tire_size, chain: chain, flag: flag}
從父類別傳送Hook
訊息的好處在於~ 讓子類別不知道演算法如何進行。 可以發現我們在父類別設立了子類別的專屬位置,讓子類別可以實作相符方法來提供資訊,換言之就是控制權返回給父類別。
差別在於許多子類別都獲得控制權的狀況下,導致每個子類別都要熟悉父類別,複雜的邏輯分散在各個子類別內。而父類別擁有控制權時,演算邏輯匯聚在抽象層內,更好整理與理解。
聽到這裡,是不是開始頭昏眼花了... 真心覺得繼承概念很好理解,但設計方式就是一門學問了!
明天再來分享另一個概念-模組(module)囉~
感謝大家 如有問題,再煩請大家指教!