iT邦幫忙

2023 iThome 鐵人賽

DAY 13
0
自我挑戰組

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

Day13 CH6 藉由繼承取得行為(下)

  • 分享至 

  • xImage
  •  

找出抽象類別

建立抽象父類別

BicycleMountainBikeRoadBike的父類別,Bicycle包含共同的行為,而MountainBikeRoadBike則用來增加特定行為。Bicycle的公共介面包含了sparessize,而其子類別的介面才是增加各自的專屬零件。Bicycle是抽象的,所以不會定義一輛完整的自行車,它只會包含所有自行車的共同內容。

抽象類別常作為父類別而存在,這是它們的唯一用途。它們提供了一個共同的行為集合,可以被一組子類別所共用,而這些子類別則提供特殊化行為。

建立新層次結構起手式:暫時先忽略程式碼的適當性,完成這項修改的最簡單作法,是將Bicycle重新命名為RoadBike,並重新建立一個空Bicycle類別。

class Bicycle
	# 這個類別現在為空。
	# 所有程式碼都被移到RoadBike。 
end

class RoadBike < Bicycle
	# 現在是Bicycle的子頫別。
	# 包含所有來白舊Bicycle類別的程式碼 
end

class MountainBike<Bicycle
	# 仍然是Bicycle(現在為空)的一個子類別。 
	# 程式碼未發生變化
end

新的RoadBike類別被定義成了Bicycle的子類別。既有的MountainBike類別已經是Bicycle的子類別。它的程式碼雖未經修改,但是其行為顯然改變了,原先MountainBike所依賴的程式碼已經從其父類別裡移除,並被放到一個對等類別裡。

提升抽象行為

由於sizespares是所有自行車的基本配備,因此要將他抽到父類別,也就是Bicycle類別,將size行為提升至父類別需要做三項修改,具體內容如下面的範例所示:

  • attr_readerRoadBike提升到Bicycle
  • initialize程式碼從RoadBike提升到Bicycle
  • RoadBikeinitialize裡增加了一個super傳送

現在當RoadBike接收到size訊息,他會將任務委派給父類別,在Bicycle去實作size

class Bicycle
  attr_reader :size # <- 推取自 RoadBike

  def initialize(args={})
	@size - args[:size] # <-捅取自RoadBike
  end 
end

class RoadBike < Bicycle
  attr_reader :tape_color

  def initialize(args)
	@tape_color = args[:tape_color]
	super(args) # <- RoadBike現在必須傳送「super」 
  end
  # ...
end

在修改之前,RoadBike能正確地冋應size ,但MountainBike不行。而現在它們的共同行為現在已經被定義在它們的父類別Bicycle裡面。繼承的神奇之處在於,現在它們都可以像下面所示那樣正確地回應size

road_bike = RoadBike.new(
  size: 'M', 
  tape_color: 'red')

road_bike.size #'M'

mountain_bike = Mountain.new(
  size: 'S',
  front_shock: 'Manitou',
  rear_shock: 'Fox')

mountain_bike.size # -> 'S

請注意,這段處理自行車尺寸的程式碼已被移動「兩次」。它最初是在Bicycle類別裡,後來被下移到了RoadBike,而現在又被提升到Bicycle。雖然這段程式碼沒有變化,但被移動「兩次」是具有意義的,為什麼不一開始就讓這段程式碼留在Bicycle裡呢?這種 「先全部下放,再部分提升」 的策略是這項重構的重點。許多繼承的難處都是因爲未能嚴格區分具體與抽象而導致的。

如果從Bicycle的第一個版本開始重構,並試圖將具體程式碼隔離起來然後下移到RoadBike,那麼若是你有任何遺漏,都會在父類別裡遺留危險的具體殘餘。但如果從一開始便將Bicycle的所有程式碼移到RoadBike,那麼你就可以仔細地辨別出抽象部分並進行提升,並且無須擔心會有具體的部份殘留在父類別裡。

重構一個繼承層次結構的一般原則是 提升抽象 而不是將 具體下移

從具體分離出抽象

分離重點:抽象部份將被提升到Bicycle,而具體部分則繼續留在RoadBike裡。

先暫時不去考量整個spares方法,集中精力來提升所有自行車都應共用的內容,即chaintire_size。如同size一樣,它們都是屬性,並且都應該使用attr_accessor來表示,而非使用寫死的值。具體的要求如下:

  • 所有白行車都有鏈條和輪胎尺寸。
  • 所有自行車都共用相同的鏈條預設值。
  • 所有子類別都提供它們自己的預設輪胎尺寸。
  • 允許子類別的具體實例忽略預設值,並提供特定於實例的值。
class Bicycle
  attr_reader :size, :chain, :tire_size

  def initialize(args={})
	@size = args[:size]
	@chain = args[:chain] 
	@tire_size = args[:tire_size]
  end
	
  def default_chain # <-共同的預設值 
	'10-speed'
  end
end

class RoadBike < Bicycle
 #...
 def default_tire_size # <-子類別預設值 
  '23'
 end
end

class MountainBike < Bicycle
  #...
  def default_tire_size # <-子類別預設值 
	'2.1'
  end
end

使用範本方法模式

將預設值封裝在方法裡,Bicycle傳送這些訊息的主要目的是讓了類別有機會覆蓋它們,以提供特殊化。

MountainBikeRoadBike提供了它們自己的預設輪胎尺寸,但繼承了共同的鏈條預設值。

class Bicycle
  attr_reader :size, :chain, :tire_size
  def Initialize(args ={})
	@size = args[:size]
	@chain = args[:chain] || default_chain
	@tire_size = args[:tire_size] || defau1t_tire_size
  end

  def default_chain # <-共同的預設值 
	'10-speed'
  end
end

class RoadBike < Bicycle
 #...
 def default_tire_size # <-子類別預設值 
   '23'
 end
end

class MountainBike < Bicycle
  #...
  def default_tire_size # <-子類別預設值 
	'2.1'
  end
end

road_bike = RoadBike.new(
  size: 'M',
  tape_color: 'red')

p road_bike.tire_size # => '23'
p road_bike.chain # => "10-speed"

mountain_bike = MountainBike.new(
  size: 'S',
  front_shock: 'Manitou',
  rear_bike: 'Fox')

p mountain_bike.tire_size # => '2.1 14 
p mountain_bike.chain # => "10-speed"

現在有一個新的情境:如果有某位程式設計師在不知情的情況下想要建立一個新的RecuirtoentBike子類別, 卻忽略了 default_tire_size實作,會產生以下錯誤:

class RecumbentBike < Bicycle
  def default_chain
	'9-speed'
  end
end 

bent = RecunibentBike.new
# NameError: undefined local variable or method 09 
# ' default_tire_size'
  • 明確地說明子類別必須實作某則訊息,這樣的程式碼能夠提供有用的文件記錄,並且就算沒有閱讀程式碼也能夠得到有用的錯誤提示訊息。
class Bicycle
  def default_tire_size 
	raise NotImplementedError
	  "This #{self.class} cannot respond to:"
  end
end
bent = RecximbentBike.new
# NotlnplementedError:
# This RecumbentBike cannot respond to: 
# 'default_tire_size'

理解耦合

spares的第一種實作撰寫起來最為簡單,但也會產生最緊密耦合的類別。既然Bicycle現在可以傳送訊息來取得鏈條和輪胎尺寸,並且其spares實作應該傳回一個散列,那麼增加下面的spares方法即可滿足MountainBike的需求。

class Bicycle
#...
  def spares
    {tire_size: tire_size, chain: chain)
  end 
end
class RecumbentBike < Bicycle
  attr_reader :flag

  def initialize(args)
	@flag = args[:flag] # 忘記傳送 super
  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')
bent.spares

# -> {:tire_size => nil, <-未進行初始化
#     :chain => nil,
#     :flag => "tall and orange"}

RecumbentBike在執行initialization期間未能傳送super時,它便會漏掉由Bicycle提供的共同初始化作業,因此無法取得有效的尺寸、鏈條和輪胎尺寸。

參考資料:

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

上一篇
Day12 CH6 藉由繼承取得行為(上)
下一篇
Day14 CH7使用模組共用角色行爲(上)
系列文
入坑 RoR 必讀 - Ruby 物件導向設計實踐30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言