iT邦幫忙

2023 iThome 鐵人賽

DAY 6
0
自我挑戰組

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

Day6 CH3 管理依賴關係(上)

  • 分享至 

  • xImage
  •  

關於物件與依賴

如果物件依賴於另一個物件,那麼當其中一個物件有所變化時另一個可能也會被迫發生變化。依賴關係會造成耦合,兩個物件耦合得越緊密,它們的行爲就越像一個單一實體,好比以前在生物課學到的共生概念,兩個物件相互依賴,就如同共生般生死與共的高度緊密關係。

如何形成依賴關係?

Gear會因Wheel變化而被迫變化的狀況,可以看出GearWheel的依賴,削弱Gear,使它難以修改。

class Gear
	attr_reader :chainring, :cog :rim, :tire 

	def initialize(chainring, cog, rim, tire)
		@chainring = chainring
		@cog = cog
		@rim = rim
		@tire = tire
	end
	def gear_inches
		ratio * Wheel.new(rim, tire).diameter
	end

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

class Wheel
	attr_reader :rim, :tire

	def initialize(rim, tire)
		@rim = rim
		@tire = tire
	end
	
	def diameter
		rim + (tire * 2)
	end
#...
end

Gear.new(52, 11, 26, 1.5).gear_inche

當物件知道以下內容,便會形成依賴關係:

  • 另一個類別的名稱,Gear期望存在有一個名叫Wheel的類別。
  • 訊息 (方法) 的名稱,Gear期望有一個Wheel實例回應diameter
  • 訊息 (方法) 所要求的參數,Gear知道Wheel.new需要rimtire
  • 參數的順序,Gear知道Wheel.new的第一個參數應該是rim ,第二個參數為tire

這些依賴關係會將Gear類別與Wheel類別耦合在一起。換個說法,即每一個耦合都會建立依賴關係。GearWheel知道得細節或訊息越多,它們的耦合就越緊密。迫使原本物件很小的微調轉變成大事件,而需要大量修改,這是在開發上最不樂見的情況,開發上應盡量避免。

難分難捨的「耦合」?

在軟體開發中,**「耦合」(Coupling)**是指兩個或多個模組、類別、物件等之間的相依程度或連結程度。
更具體地說,耦合表示一個模組對於其他模組的了解程度,以及兩個模組之間的交互程度。耦合程度高意味著模組之間的相互影響程度大,而耦合程度低則表示模組之間更加獨立且容易修改。
高度耦合的系統容易產生連鎖效應,一個小的變更可能需要修改大量相依的程式碼,並可能引入錯誤。低耦合度則有助於減少影響範圍,使程式碼更易於理解、修改和測試。
因此,在設計軟體系統時,通常會追求鬆散耦合的架構。

如何撰寫低耦合的程式碼?

書中提及三個手法,分別是依賴注入、 隔離依賴關係、移除參數順序的依賴關係。

1.依賴注入
其意義在於知道某個類別名稱與知道傳送給該類別的訊息名稱是分屬於不同物件的責任。

Before
如果Gear自身知道Wheel的名稱,那麼當Wheel的名稱發生變化時,在Gear裡的那段程式碼也必須進行調整。更進一步,當Gear將對Wheel的引用寫死並深入到gear_inches方法,那麼這便是明確地宣告它只願意為 Wheel的實例計算齒輪英寸數,導致其他類型的物件無法使用此方法。

class Gear
	attr_reader :chainring, :cog, :rim, :tire
	def initialize(chainring, cog, rim, tire)
		@chainring = chainring 
		@cog = cog
		@rim = rim
		@tire = tire
	end

	def gear_inches
		ratio * Wheel.new(rim, tire).diameter
	end
	# ...
end

# Gear 期望一個知道「diameter」的「鴨子類型」 
Gear.new(52, 11, 26, 1.5).gear_inches

After
將建立Wheel新實例移動到Gear之外就是解耦兩個類別,Gear可以跟任何有實作diameter物件的物件傳遞訊息,而不被Wheel限制或影響。Gear只需要存取一個可以回應diameter的物件。Gear可以不知道這個物件是否就是Wheel類別的一個實例,只知道要保存一個會回應diameter的物件(@wheel) 就行。

class Gear
	attr_reader :chainring, :cog, :wheel
	def initialize(chainring, cog, wheel)
		@chainring = chainring 
		@cog = cog
		@wheel = wheel
	end

	def gear_inches
		ratio * wheel.diameter
	end
	# ...
end

# Gear 期望一個知道「diameter」的「鴨子類型」 
Gear.new(52, 11, Wheel.new(26, 1.5)).gear_inches

2.隔離依賴關係
在條件不允許,無法解耦的情況下,將不必要的依賴關係隔離起來,避免類別受到污染,等時機成熟時再做移除。先減少 gear_inches裡的依賴關係數量,同時公開GearWheel的依賴。降低重複使用這段程式碼的門檻,並讓它在條件允許時更容易重構。

  • 隔離實例建立
    如果受到嚴重的束縛,導致無法藉由修改將Wheel注入進Gear,就在Gear內部將建立新Wheel實例的動作隔離起來。

    1.每次建立新的Gear時無條件地建立一個新的Wheel

      class Gear
        attr_reader :chainring, :cog, :rim, :tire
        def initialize(chainring, cog, rim, tire)
      	@chainring = chainring 
      	@cog = cog
          @wheel = Wheel.new(rim,tire)
        end
    
        def gear_inches
      	ratio * wheel.diameter
        end
      end
    

    2.新Wheel實例的建立會在gear_inches呼叫新wheel方法時才執行。

      class Gear
        attr_reader :chainring, :cog, :rim, :tire
        def initialize(chainring, cog, rim, tire)
      	@chainring = chainring 
      	@cog = cog
      	@rim = rim
      	@tire = tire
        end
    
        def gear_inches
      	ratio * wheel.diameter
        end
    
        def wheel
      	@wheel ||= Wheel.new(rim, tire)
        end
        # ...
      end
    
      # Gear 期望一個知道「diameter」的「鴨子類型」 
      Gear.new(52, 11, Wheel.new(26, 1.5)).gear_inches
    
  • 隔離脆弱的外部訊息

    Before
    假設對gear_inches的計算需要更多的數學運算,現在wheel.diameter被深層嵌入在一個複雜的方法裡。 gear_inches知道wheel有一個diameter,這是一種很危險的依賴關係,會將gear_inches與外部物
    件以及它的一個方法耦合在一起。

    def gear_inches
      foo = some_intermediate_result * wheel.diameter
    end
    

    After
    我們必須移除外部依賴關係,並將其封裝在某個方法裡(diameter),試著降低修改gear_inches的機率

    def gear_inches
      foo = some_intermediate_result * diameter
    end
    
    def diameter
      wheel * diameter
    end
    

    在完成這項修改之後gear_inches變得更加抽象。Gear現在將wheel.diameter隔離在一個單獨的方法 裡,並且gear_inches可以依賴於某則傳送給self的訊息。 如果Wheel修改了diameter方法實作的內 容,那麼對Gear的副作用將會被限制在這個簡單的包裝方法裡。

3.移除參數順序的依賴關係

  • 使用散列表初始化參數
    為了避免Gear建立時依賴於順序固定的參數,initialize方法現在只接受一個參數args,它是一個包含了所有輸入資料的散列表。Gear現在可以自由地增加或移除初始化參數和預設值,同時也能夠放心進行任何修改。

    class Gear
      attr_reader :chainring, :cog, :wheel 
      def initialize(args)
      	@chainring = args[:chainring]
      @cog = args[:cog]
      	@wheel = args[:wheel]
      end
      #...
    end
    
    class Wheel
    attr_reader :rim, :tire
    
    def initialize(rim, tire)
      @rim = rim
      @tire = tire
    end
    end
    
    Gear.new(
      :chainring => 52,
      :wheel => Wheel.new(26, 1.5),
      :cog=>11)
    #<Gear:0x000000012687d378 @chainring=52, @cog=11, @wheel=#<Wheel:0x000000012687d3f0    
    @rim=26, @tire=1.5>>
    

    假設即要求方法擁有多個非常穩定的參數,同時還要能選擇性允許一些不穩定的參數存在。在這種情況下,兩種技巧
    都使用,即接受多個順序固定的參數,後面再緊跟一個選項散列表。

  • 明確地定義預設值

  1. 使用「| |」方法
    對於簡單非布林型的預設值可以使用Ruby的「| |」方法來指定,以取得參數值(args[:chainring])為優先,如果參數值為nilfalse則使用預設值(40)。
# 使用 || 來提供預設値
def initialize(args)
	@chainring = args[:chainring] || 40
	@cog = args[:cog] || 18
	@wheel = args[:wheel]
end
  1. 使用**fetch方法
    當需要將布林值作為參數,或者需要明確區分參數是否為falsenil,可以使用
    fetch方法來設定預設值。fetch**方法可以更精確地處理預設值,參數不存在,則自動返回nil
# 使用 fetch 來指定預設値
def initialize(args)
	@chainring = args.fetch(:chainring, 40)
@cog = args.fetch(:cog, 18)
@wheel = args[:wheel]
end
  1. 隔離預設值設定
    將預設值從**initialize**方法中提取到一個單獨的包裝方法。如果預設值不僅僅是一些簡單的數值或字串,最好是實作一個defaults方法做隔離。mergefetch具有相同的效果,當它們的值不在散列表裡時,這些預設值才會被合併。
def initialize(args)
  args = defaults.merge(args)
  @chainring = args[:chainring]
  # 其他參數的處理
end

def defaults
  {:chainring => 40, :cog => 18}
end
  • 隔離多重參數初始化操作
    有時候,你會使用某個外部庫或框架提供的類別,但這些類別的初始化方法對參數順序有要求,你無法修改它們。必須:
  1. 封裝外部依賴:
    為了避免多重依賴關係和提高代碼的可維護性,可以創建一個包裝器(wrapper)來封裝外部依賴。這個包裝器可以是一個模塊,負責使用外部類創建實例。

  2. 工廠模式:
    包裝器的作用類似於工廠模式,因為它的主要目的是創建其他類的實例。工廠模式是一種設計模式,用於將對象的創建與使用分離,從而更容易維護和修改代碼。對工廠模式有興趣可以點這裡去看更詳盡的介紹。

  3. 建立Gearwrapper模組:
    SomeFramework::Gear類是app外的一部分,其初始化方法需要固定順序的參數。通過創建GearWrapper模塊,可以將外部依賴關係隔離,並為應用程序提供更改良的接口,避免對這些參數的順序產生多重依賴關係。Gearwrapper將所有外部介面的知識都隔離在一處,它還為你的應用程式提供了一個改良過的介面。

  module SomeFramework
    class Gear
      attr_reader :chainring,:cog, :wheel 
      def initialize(chainring, cog, wheel)
        @chainring = chainring 
        @cog = cog
        @wheel = wheel
      end
      #...
    end
  end

 #將該介面包裝起來,以保護自己不會受其修改的影響
 module GearWrapper
	def self.gear(args)
		SomeFramework::Gear.new(args[:chainring],
		  args[:cog],
		  args[:wheel])
	end
 end

 # 現在可以使用參數散列表來建立新的 Gear
  p GearWrapper.gear(
    :chainring => 52,
    :cog=>11,
    :wheel => Wheel.new(26, 1.5))

  #<SomeFramework::Gear:0x00007fdd3a130050 @chainring=52, @cog=11, @wheel=# 
  <Wheel:0x00007fdd3a130118 @rim=26, @tire=1.5>>

這項作法的好處在於當你需要使用外部類但無法修改其參數順序時,封裝外部依賴關係並使用工廠模式是一種有效的解決方法,可以保護你的代碼免受外部依賴的影響。

參考資料

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

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

尚未有邦友留言

立即登入留言