iT邦幫忙

2023 iThome 鐵人賽

DAY 5
0
自我挑戰組

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

Day5 CH2 設計具有單一職責的類別 (下)

  • 分享至 

  • xImage
  •  

瞭解單一職責的內容後,我們可以進一步修正,讓程式碼的調整彈性更高、更易於修改,書中提及兩種修正方向:
1.依賴行為而非資料
2.全面實施單一職責

依賴行為而非資料

  1. 隱藏實例變數
  • 將變數包裝在方法裡,可以將它們隱藏起來,甚至連定義它們的類別也無法看到它們,Ruby提供了 attr_reader ,它可以作為簡單的封裝建立方法
  • 將每一個實例變數都包裝在方法裡,進而將任何變數都當成是物件。於是,資料和正規物件之間的區別開始消失。
class Gear
  attr_reader :chainring, :cog # <----------- 

  def initialize(chainring, cog)
    @chainring = chainring
    @cog = cog
  end

  def ratio
    chainring / cog.to_f # <-----------
  end 
end

p gear = Gear.new(4,2) #<Gear:0x000000013a81aed0 @chainring=4, @cog=2>
p gear.ratio #2.0
  1. 隱藏資料結構
    attr_reader 將實例變數@data包裝在方法裡。diameters方法傳送data訊息來存取變數的內容。
class ObscuringReferences
	attr_reader :data 
	def initialize(data)
		@data = data
	end

	def diameters
		# 0 代表鋼圈,1 代表輪胎 
		data.collect {|cell|
			cell[0] + (cell[l] * 2)}
	end
	# ...其他大量索引該陣列的方法
end

不過,由於 @data 包含複雜的資料結構,所以只有隱藏實例變數還不夠。它依賴於陣列的結構,如果結構發生變化那麼這段程式碼就必須修改。修改方向如下:

  1. 使用方法封裝結構:在Ruby中,可以使用Struct類別來封裝複雜的結構,將其轉換成物件。在RevealingReferences類別中,wheelify方法使用Struct將來源陣列轉換成結構化的物件陣列。
  2. 隔離結構細節wheelify方法內部隔離了來源陣列的結構細節,使得程式碼只需在一個地方進行修改,而不需要在多處修改。
  3. 提高程式碼可讀性:這種做法不受外部資料結構變化的影響,同時也提高了程式碼的可讀性,這種封裝方式讓類別更適應不同的變化,並隱藏了混亂的結構細節。
class RevealingReferences
  attr_reader :wheels

  def initialize(data)
    @wheels = wheelify(data)
  end

  def diameters
    wheels.collect { |wheel|
      wheel.rim + (wheel.tire * 2)}
  end.  

  Wheel = Struct.new(:rim, :tire)
  def wheelify(data)
    data.collect{ |cell|
      Wheel.new(cell[0], cell[1])}
  end

  p RevealingReferences.new([[1,2],[2,3]]) #<RevealingReferences:0x000000012908ce18 @wheels=[#<struct RevealingReferences::Wheel rim=1, tire=2>, #<struct RevealingReferences::Wheel rim=2, tire=3>]>
  p RevealingReferences.new([[1,2],[2,3]]).wheels #[#<struct RevealingReferences::Wheel rim=1, tire=2>, #<struct RevealingReferences::Wheel rim=2, tire=3>]
  p RevealingReferences.new([[1,2],[2,3]]).diameters #[5, 8] 

end

全面實施單一職責

與類別相同,方法也應該具有單一職責。

1.從方法撷取出額外職責
RevealingReferences類別的diameters方法為例:

def diameters
  wheels.collect {|wheel|
end

wheel.rim + (wheel.tire * 2)}

將程式碼簡化成兩個獨立的方法,各自擔負一項職責。

#一、迭代陣列
def diameters
  wheels.collect {|wheel| diameter(wheel)} 
end

#二、計算一個輪子的直徑
def diameter(wheel)
	Wheel.rim + (wheel.tire * 2))
end

重新呼叫Gear類別的gear_inches方法:

輪子直徑的計算工作是隱藏在gear_inches的內部。

def gear_inches
	# 鋼圈加上圍繞的輪胎即弓輪子直徑
	ratio * (rim + (tire * 2))
end

將這項工作撷取到新的diameter方法裡,這樣針對類別的職責檢査就會變得更容易。

def gear_inches
	ratio * diameter 
end

def diameter
	rim + (tire * 2) 
end

重點摘要:

  • 揭露出先前隱藏的性質:
    讓類別只為單一目的服務,使類別要實作的事物更加清晰。
  • 避免使用註解:
    因為註解本身無法執行,應使新隔離的方法達成與註解相同的效果。
  • 鼓勵重複使用:
    小型的方法讓其他程式設計師將重複使用這些方法,而不是對程式碼進行複製,基於你所制定的模式,繼續建立出小型、可重複使用的方法。
  • 易於移動到另一個類別:
    當你獲得更多設計資訊並決定做出調整時,小型的方法更容易移動。

2.隔離類別的額外職責
Gear類別擁有一些像是輪子的行爲。那麼這支應用程式會需要一個Wheel類別嗎?

其實答案應該要是肯定的,當應用程式對Wheel類別有了明確的需求,它必須要能夠獨立在Gear之外使用,假設未來有其他需求是處理wheel的大小事務,也都可以統一交由Wheel類別負責。

時間來到未來,你需要「計算自行車輪子周長」,由於你已經很小心地將Gear類別裡的Wheel行為隔離起來,所以這項修改是無痛的。

Wheel Struct 簡單地轉換成一個獨立的Wheel類別,並增加新的circumference方法:

class Gear
	attr_reader :chainring, :cog, :wheel

	def initialize(chainring, cog, wheel=nil)
		@chainring = chainring
		@cog = cog
		@wheel = wheel
	end

	def ratio
		chainring / cog.to_f
	end

	def gear_inches
		ratio * wheel.diameter
	end
end

class Wheel
	attr_reader :rim, :tire

	def initialize(rim, tire)
		@rim = rim
		@tire = tire
	end
	
	def diameter
		rim + (tire * 2)
	end
	
	def circumference
		diameter * Math::PI
	end
end

@wheel = Wheel.new(26, 1.5)
puts wheel.circumference
# -> 91.106186954104 38

puts Gear.new(52, 11, @wheel).gear_inches
# -> 137.090909090909

puts Gear.new(52, 11).ratio
# -> 4.72727272727273

結論

我們應該努力實踐單一職責類別或方法,讓只負責單一事物的類別或方法能夠將事物與應用程式的其他部分有所隔離,確保程式碼保有在未來修改或擴增的彈性。

參考資料:

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

上一篇
Day4 CH2 設計具有單一職責的類別(上)
下一篇
Day6 CH3 管理依賴關係(上)
系列文
入坑 RoR 必讀 - Ruby 物件導向設計實踐30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言