iT邦幫忙

2023 iThome 鐵人賽

DAY 12
0
自我挑戰組

入坑 RoR 必讀 - Ruby 物件導向設計實踐系列 第 12

Day12 CH6 藉由繼承取得行為(上)

  • 分享至 

  • xImage
  •  

設計良好的應用程式由可重複使用的程式碼所構成。

在物件導向程式設計中,「可重複」就是倚賴繼承的方式來實現整個應用程式的運作,繼承是一種程式碼共用技巧,通常內建於許多面向物件的語言中。繼承允許建立富含技巧的繼承層次結構,正確使用繼承可以促進程式碼的重用和結構化。

在具備前5章的知識後,我們可以理解到遵守物件導向設計的程式,通常是小型的、值得信賴且自給自足的物件,在具有最少上下文、清晰的介面和注入依賴關係時,自然是可重複使用的。當符合以上原則後便可以進一步來實作繼承。

瞭解經典繼承

繼承是一種用於實現「訊息自動委派」的機制。它定義了一條轉遞路徑,使當物件無法回應某訊息時,該訊息可以自動轉遞給另一個物件,而無需明確編寫委派程式碼。

繼承建立了類別之間的關係,通常是子類別和父類別之間的關係,其中子類別可以繼承父類別的行為,當子類別無法處理某訊息時,它可以委派該訊息給其父類別處理。

識別繼承

實作演練

以 FastFeet 公司應用程式中的Bicycle類別為例。Bicycle實例可以知曉sparessizetape_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_colorrear_shock之前先檢查style

找出嵌入的類型

style 變數有效地將 Bicycle 實例劃分為兩種不同的事物,這兩種事物在行為上有許多相似之處,只在 style 層面有所不同。這種情況正是繼承機制要解決的問題,即多種相關類型共享某些行為但在某些方面有所不同。

總之,style 變數的使用提示出了繼承在處理多種相關類型時的重要性,因為它們可能共享大部分相似行為,但在某些方面有所不同,需要適當的抽象和設計模式來處理。

選擇繼承

所謂的「選擇」在此為繼承提供了一種方式:將兩個物件定義成具有一定的關係,這樣,當第一個物件接收到無法理解的訊息時,它會將訊息自動轉遞或委派給第二個物件。
Ruby支援單一繼承(即一個子類別只允許一個父類別),即使你自己從未明確地建立過類別層次結構,你仍然使用到繼承。當定義新類別時,儘管沒有特別指定它的父類別,Ruby 仍會自動將新類別的父類別設定為Object, 也就是說,其實你所建立的每個類別預設都是某個事物的子類別。

當物件接收到無法理解的訊息時,Ruby會自動將該訊息轉遞給父類別鏈,搜尋與之符合的方法實作。

錯誤繼承

MountainBike類別是Bicycle類別的直系後代。它實作了兩個方法initializespares。由於這兩個方法已經在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會繼承太多不需要的行為。

參考資料:

  • Practical Object-Oriented Design in Ruby: An Agile Primer

上一篇
Day11 CH6 使用鴨子類型技巧降低成本(下)
下一篇
Day13 CH6 藉由繼承取得行為(下)
系列文
入坑 RoR 必讀 - Ruby 物件導向設計實踐30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言