設計良好的應用程式由可重複使用的程式碼所構成。
在物件導向程式設計中,「可重複」就是倚賴繼承的方式來實現整個應用程式的運作,繼承是一種程式碼共用技巧,通常內建於許多面向物件的語言中。繼承允許建立富含技巧的繼承層次結構,正確使用繼承可以促進程式碼的重用和結構化。
在具備前5章的知識後,我們可以理解到遵守物件導向設計的程式,通常是小型的、值得信賴且自給自足的物件,在具有最少上下文、清晰的介面和注入依賴關係時,自然是可重複使用的。當符合以上原則後便可以進一步來實作繼承。
繼承是一種用於實現「訊息自動委派」的機制。它定義了一條轉遞路徑,使當物件無法回應某訊息時,該訊息可以自動轉遞給另一個物件,而無需明確編寫委派程式碼。
繼承建立了類別之間的關係,通常是子類別和父類別之間的關係,其中子類別可以繼承父類別的行為,當子類別無法處理某訊息時,它可以委派該訊息給其父類別處理。
以 FastFeet 公司應用程式中的Bicycle類別為例。Bicycle實例可以知曉spares、size和 tape_color並回傳訊息。藉由向每一個Bicycle詢問spares的方式,我們能夠得知每台自行車的基本資訊。
class Bicycle
attr_reader :size, :tape_color
def initialize(args)
@size = args[:size]
@tape_color = args[:tape_color]
end
# 每一輛自行車都有相同的
# 輪胎和鏈條尺寸預設值
def spares
{chain: '10-speed',
tire_size: '23',
tape_color: tape_color}
end
#許多其他方法...
end
bike = Bicycle.new(
size: 'M',
tape_color: 'red')
p bike.size #"M"
p bike.spares #{:chain=>"10-speed", :tire_size=>"23", :tape_color=>"red"}
假設FastFeet公司又開始舉辦山地自行車旅程,又會發生什麼事情呢?
修改現有的Bicycle類別,以便讓spares一併支援公路自行車和山地自行車。
class Bicycle
attr_reader :style, :size, :tape_color, :front_shock, :rear_shock
def initialize(args)
@style = args[:style]
@size = args[:size]
@tape_cclor = args[:tape_color]
@front_shock = args[:front_shock]
@rear_shock = args[:rear_shock]
end
#對「style」進行檢査會是條不歸路
def spares
if style == :road
{chain: '10-speed',
tire_size: '23', #毫米
tape_color: tape_color}
else
{chain: '10-speed',
tire_size: '2.1', # 英寸
rear_shock: rear_shock}
end
end
end
bike = Bicycle.new(
style: :mountain,
size: 'S',
front_shock: 'Manitou',
rear_shock: 'Fox')
p bike.spares #{:chain=>"10-speed", :tire_size=>"2.1", :rear_shock=>"Fox"}
已有的 Bicycle 類別被修改,以包含新的變數和存取器,以及對 spares 方法的更改。這種修改可能會破壞原本的程式碼,並引入錯誤風險。
這時可能犯了一個寫程式常見的錯誤—反面模式,反面模式是指一種表面上看似有好處但實際上卻有害的常見模式。試圖修改現有的 Bicycle 類別以支援新需求就是反面模式,違反了單一責任原則,style 變數被引入以區分不同風格的自行車。這導致了 spares 方法中的條件分支,需要根據 style 值選擇要包含哪些備件,增加了物件間的依賴關係,這會導致持有Bicycle實例的物件,可能得在傳送tape_color或rear_shock之前先檢查style。
style 變數有效地將 Bicycle 實例劃分為兩種不同的事物,這兩種事物在行為上有許多相似之處,只在 style 層面有所不同。這種情況正是繼承機制要解決的問題,即多種相關類型共享某些行為但在某些方面有所不同。
總之,style 變數的使用提示出了繼承在處理多種相關類型時的重要性,因為它們可能共享大部分相似行為,但在某些方面有所不同,需要適當的抽象和設計模式來處理。
所謂的「選擇」在此為繼承提供了一種方式:將兩個物件定義成具有一定的關係,這樣,當第一個物件接收到無法理解的訊息時,它會將訊息自動轉遞或委派給第二個物件。
Ruby支援單一繼承(即一個子類別只允許一個父類別),即使你自己從未明確地建立過類別層次結構,你仍然使用到繼承。當定義新類別時,儘管沒有特別指定它的父類別,Ruby 仍會自動將新類別的父類別設定為Object, 也就是說,其實你所建立的每個類別預設都是某個事物的子類別。
當物件接收到無法理解的訊息時,Ruby會自動將該訊息轉遞給父類別鏈,搜尋與之符合的方法實作。
MountainBike類別是Bicycle類別的直系後代。它實作了兩個方法initialize和spares。由於這兩個方法已經在Bicycle裡實作過,因此MountainBike會覆蓋掉它們。
在下面的程式碼裡,每一個覆蓋後的方法都會傳送 super。
class MountainBike < Bicycle
attr_reader :front_shock, :rear_shock
def initialize(args)
@front_shock = args[:front_shock]
@rear_shock = args[:rear_shock]
super(args)
end
def spares
super.merge(rear_shock: rear_shock)
end
end
mountain_bike = MountainBike.new (
size: 'S',
front_shock: 'Manitou',
rear_shock: 'Fox')
p mountain_bike.size # 'S'
p mountain_bike.spares
# {:tire_size => "23", <-錯了
:chain => "10-speed"
:tape_color => nil, <-不適用
:front_shock => 'Manitou',
:rear_shock => "Fox"}
將新的MountainBike類別直接塞入現有的Bicycle類別之下,MountainBike的實例混亂地包含公路自行車和山地自行車的行為。
因為Bicycle類別是一個具體類別,它原先的設計並不是要成為父類別。所有自行車的通用行為與僅針對公路自行車的行為現在都合在一起了。當你將MountainBike丟至Bicycle下方時,你便繼承了全部的行為,它表明公路自行車的行為被嵌入在Bicycle內部。這項安排導致MountainBike會繼承太多不需要的行為。
參考資料: