iT邦幫忙

2023 iThome 鐵人賽

DAY 17
0

組合 Bicycle

  • Bicycle有一個Parts,而Parts則有一個Part物件集合。
  • Parts是一個扮演Parts角色的類別,它實作spares
  • Part的角色則由OpenStruct扮演,它會實作namedescriptionneeds_spare
  • spares現在會傳回一個像Part物件的陣列
class Bicycle
  attr_reader :size, :parts

  def initialize(args ={}) 
    @size = args[:size] 
    @parts = args[:parts]
  end

  def spares
    parts.spares 
  end
end

require 'forwardable'
class Parts
  extend Forwardable
  def_delegators :@parts, :size, :each 
  include Enumerable

  def initialize (parts) 
    @parts = parts
  end

  def spares
    select {|part| part.needs_spare)
  end
end

require 'ostruct' 
module PartsFactory
  def self.build(config, parts_class = Parts) 
	parts_class.new(
	  config.collect {|part_config| 
		create_part(part_config)})
  end

  def self.create_part(part_config) 
	OpenStruct. new (
	  name: part_config[0],
	  description: part_config[1], 
	  needs_spare: part_config.fetch(2, true))
  end 
end

road_config =
  [['chain', ' 10-speed'],
  ['tire_size', '23 '], 
  ['tape_color', 'red']]

mountain_config = 
  [['chain', '10-speed'], 
  ['tire size', '2.1'],
  ['front shock', 'Manitou', false],
  ['rear_shock', 'Fox']]

road_bike = 
  Bicycle .new( 
	size: 'L',
	parts: PartsFactory.build(road_config))

road_bike.spares

# -> [#<OpenStruct PartsFactory::Part name="chain", 等等......

mountain_bike =
  Bicycle .new(
	size: 'L',
	parts: PartsFactory.build(mountain_config))

mountain_bike.spares
# -> [#<OpenStruct name="chain",

有了這些新類別,便可以輕易建新的自行車類型:

recumbent_config =
  [['chain', '9-speed'],
  ['tire_size', '28'],
  ['flag', 'tall and orange']]

recumbent_bike = 
  Bicycle .new( 
	size: 'L',
	parts: PartsFactory.build(recumbent_config))

recumbent_bike.spares
# -> [#<OpenStruct 
#.      name="chain",
#.      description="9-speed", 
#.      needs_spare=true>,
#.    #<OpenStruet
#.      name= "tire_size", 
#.      description="28", 
#.      needs_spare=true>,
#.    #<OpenStruct
#.      name="flag",
#.      description="tall and orange", 
#.      needs_spare=true>]

繼承和組合之間的抉擇

經典繼承

行為分散在物件裡面,而物件則被組織成類別關係,以便能夠自動委派訊息來呼叫正確的行為。

組合

  • 物件之間的關係並沒有被制約在類別層次結構裡。
  • 物件是獨立存在的,必須明確地瞭解訊息,並將它們委派給另一個物件。
  • 允許物件之間的結構獨立性,需要明確地進行訊息委派。

通常,當問題可以使用組合技巧解決時,應該優先考慮使用組合。組合具有較低的依賴關係,更加靈活,適用於大多數情況。只有在確實需要共享通用行為且能夠確保低風險的情況下,才應考慮使用繼承。

使用繼承的結果

1. 繼承的獲益
滿足合理、可用和典範這三項程式碼目標。

  • 合理性
    允許我們建立合理的類層次結構,其中頂層方法的定義具有最廣泛的影響。正確建模其結構可以通過小的代碼更改來實現重大的行為變化。

  • 典範性
    正確編寫的類層次結構易於擴展,體現了抽象,並且每個新的子類都會插入某種具體的差異。這種模式容易遵循,具有典範性。

  • 可用性
    使用繼承的程式碼可以描述為“開-閉”原則,即對於擴展是開放的,但對於修改是封閉的。這意味著可以輕鬆擴展代碼以適應新的需求,而不需要修改現有代碼,可用程度高,可輕鬆創建新的子類來適應變化。

2. 繼承的代價

  • 你需要增加行為,但卻發現難以實作,由於該模型一開始便不正確,因此無法順利加 入。這種情況下,你將複製或重組程式碼。
  • 即使繼承適用於目前的問題,但有可能你正在撰寫的程式碼會被其他人 用於你完全未曾 預料到的目的。

結論:

子類別不僅依賴於其父類別裡定義的方法,還會依賴於傳送給父類別的訊息自動委派。出於設計的緣故導致無可避免的綁定在一起,因此,當修改父類別時還是會引發廣泛的行為修改。

選擇使用繼承也應考量到將來程式碼的使用者,撰寫時請避免要求使用者必須以子類別方式繼承物件才能夠獲得程式碼的行爲。

使用組合的結果

1. 組合的獲益

  • 建立出許多包含簡單責任的小物件,它們可藉由明確定義的介面來存取,這些小物件都具有單一職責,並且指定了它們自己的行為。
  • 組合物件獨立於層次結構,只繼承很少的程式碼。因此,當上游的物件被修改時,它通常能倖免。
  • 組合的物件都很小,並且有著定義良好的介面,讓應用程式能夠被建構在簡單 、可插拔的物件上,使其易於擴展 。

2. 組合的代價

一個組合物件依賴於許多部分,即使每個部分都很小且易於理解,但整個組合操作可能沒有那麼明顯,組合物件必須明確地知道哪一則訊息需要委派給誰,相同的委派程式碼可能會被許多不同的物件所需要,但組合沒有提供共用這段程式碼的方式。

選擇關係

1. 繼承用於「是什麼」的關係

物件有時會很自然地形成靜態且相當明顯的特殊化層次結構,對於這類物件可以使用經典繼承來建模。適用於淺窄的層次結構,小型的層次結構尺寸讓它易於理解,意圖明顯,且易於擴展,符合成功利用繼承的標準。

2. 鴨子類型用於「表現得像什麼」的關係

有些情況需要許多不同的物件扮演一個共同的角色。有兩種方法可用於識別出存在的角色。第一種方法,儘管某個物件扮演了這個 角色,但這個角色並非該物件的主要職責。第二種方法,有某項普遍的需求,使得許多原本並不相關的物件都期望扮演同一個角色。

3. 組合用於「含有什麼」的關係

物件包含了大量的個別部分,而並不僅僅是個別部分的總和,包含了其他行為。

「是什麼」和「含有什麼」的區別,是決定繼承或組合的重點。物件的個別部分越多,就越有可能應該使用組合來建模。

結論

  • 組合將多個小部分結合起來,建立出更複雜的物件,進而使其整體不僅僅是個別部分的總和。
  • 組合物件由簡單、零散的實體組成,它們可以輕易地重新安排成新的組合。
  • 這些物件易於理解、可重複使用和測試,但由於它們是組合成一個更爲複雜的整體,所以其整體可能並不像個別部分一樣那麼容易理解。
  • 組合、經典繼承以及藉由模組的行為共用,都是具對立性的程式碼安排技巧。

參考資料:

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

上一篇
Day16 CH8組合物件(上)
下一篇
Day18 CH9 設計節省成本的測試(上)
系列文
入坑 RoR 必讀 - Ruby 物件導向設計實踐30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言