iT邦幫忙

2022 iThome 鐵人賽

DAY 7
0
自我挑戰組

Ruby OOP to Oops !n 30系列 第 7

IT 邦鐵人賽 Day 7 - Inheritance

  • 分享至 

  • xImage
  •  

繼承(Inheritance)

其幾天我們從單一個類別,像是水平擴展一樣,討論耦合與介面等問題。
而今天我們要開始討論的是垂直擴展的繼承問題~
個人覺得繼承概念其實沒有很困難,但要注意到細節!

首先是繼承的定義

繼承(英語: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

super

class RecumbentBike < Bicycle

  def initialize(args = {})
    @flag = args[:flag]
    super
  end
#{:tire_size=>"28", :chain=>"9-speed", :flag=>"tall and orange"}
#這時候可以運作成功,但就不能忘記要加上super(更動子類別來操作父類別的參數)
end

Hook

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運作流程:

  1. RecumbentBike.new時因為RecumbentBike類別沒有initialize所以經由繼承找到Bicycle
  2. Bicycle內在initialize時執行了post_initialize(args)此時的(args)是flag: 'tall and orange'但是這個方法並不是Bicycle內的,而是RecumbentBike所以會回傳@flag = args[:flag]
  3. 然後執行bent.spares時會有.merge(local_spares)的步驟,local_spares源於RecumbentBike
  4. 回傳{flag: flag}
  5. merge{tire_size: tire_size, chain: chain, flag: flag}

從父類別傳送Hook訊息的好處在於~ 讓子類別不知道演算法如何進行。 可以發現我們在父類別設立了子類別的專屬位置,讓子類別可以實作相符方法來提供資訊,換言之就是控制權返回給父類別
差別在於許多子類別都獲得控制權的狀況下,導致每個子類別都要熟悉父類別,複雜的邏輯分散在各個子類別內。而父類別擁有控制權時,演算邏輯匯聚在抽象層內,更好整理與理解。

聽到這裡,是不是開始頭昏眼花了... 真心覺得繼承概念很好理解,但設計方式就是一門學問了!
明天再來分享另一個概念-模組(module)囉~

感謝大家 如有問題,再煩請大家指教!


上一篇
IT 邦鐵人賽 Day 6 - Duck Typing
下一篇
IT 邦鐵人賽 Day 8 - Module
系列文
Ruby OOP to Oops !n 3020
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
Mike_Lai
iT邦新手 4 級 ‧ 2022-09-23 00:01:26

呱呱

0

我要留言

立即登入留言