iT邦幫忙

2023 iThome 鐵人賽

DAY 10
0
自我挑戰組

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

Day10 CH5 使用鴨子類型技巧降低成本(上)

  • 分享至 

  • xImage
  •  

今天要來瞭解另一種介面,有別於第4章所提及的「介面」:存在於類別裡,並且由其方法所組成。另一種介面是指跨越多個類別,並獨立於單一類別的「介面」,介面代表的是一組訊息,由訊息自身定義了這個介面。

許多不同的類別作為其整體的一部分,可能是實作了介面所要求的方法,像是介面定義了某個虛擬類別,也就是說,任何類別實作了所要求的方法就可以表現得像是介面類型

鴨子類型(Duck Type)

當時在五倍上課時,龍哥問了我們什麼是鴨子型別,那是我第一次聽到鴨子型別。查了許多參考資料,加上寫程式一段時間,才讓我更加理解了鴨子類型,現在透過說明,希望對正在閱讀此篇文章的你能夠有所幫助。

依據 wiki 的定義:

If it walks like a duck and it quacks like a duck, then it must be a duck.
他走起路像鴨子,具有鴨子叫聲,我們就可以說他是鴨子。

鴨子類型(Duck type)是一種用來譬喻動態型別語言(Dynamically Typed Language)的設計風格。在這種風格中,一個對象有效的語意,不是由繼承自特定的類或實現特定的介面,而是由「當前方法和屬性的集合」決定。主要根據特徵判斷,可以提供強大的靈活性,實現 多型(polymorphism) ,有關多型將會在其他章節說明,這邊先讓我們把重點放在鴨子就好。

更白話一點來說,鴨子類型指的是不會綁定到任何特定類別的公共介面,鴨子類型物件就像是變色龍,它們是按其特徵行爲而非其類別來定義的。因此,如果物件叫起來像鴨子,走起來像鴨子,那麼其類別並不重要,它就是一隻鴨子。

理解鴨子類型

物件的使用者不需要(也不應該)關心它的類別,類別只是物件取得公共介面的一種方式。物件藉由類別方式所取得的公共介面,可能只是類別所包含的公共介面中的其中一個,一個物件可以實作多個不同的介面,這稱為「鴨子類型」。正確使用鴨子類型和跨類別的類型,可以創建出靈活的、有結構的設計,其重點不在於這個物件 是什麼,而是在於它要 做什麼,這種靈活性有助於建立更容易擴展和維護的系統。

實例演練

*本章延續上個章節的自行車例子繼續說明

  1. Tripprepare方法會傳送prepare_bicycles訊息給包含在mechanic參數裡的那個物件。在此處,儘管參數名稱為mechanic,但它所包含的那個物件可以是任何類別。

    prepare方法對Mechanic類別並沒有外顯的依賴關係,實際上,它依賴於所接收到可以回應
    prepare_bicycles的物件。

    class Trip
        attr_reader :bicycles, :customers, :vehicle
        #這個「mechanic」參數可以是任何類別 
      def prepare(mechanic)
            mechanic. prepare_bicycles (bicycles)
        end
    
        #...
    end
    
    # 如果剛好傳遞了這個類別的實例,
    # 它就可以運作
    class Mechanic
        def prepare_bicycles(bicycles)
            bieyeles.each {|bicycle | prepare_bicycle(bicycle)} 
        end
    
        def prepcire_bicycle (bicycle) 
            #...
        end
    end
    
  2. 假設需求發生了變化,除「需要技工以外,現在旅程準備還要包括一名協調員和一名司機。

    # Trip的準備工作變得很複雜  
    class Trip
        attr_reader :bicycles, :customers, :vehicle
    
        def prepare(preparers) 
            preparers.each {|preparer|
                case preparer 
                when Mechanic
                    preparer.prepare_bicycles(bicycles)
                when Tripcoordinator
                    preparer.buy_food(customers)
                when Driver
                    preparer.gas_up(vehicle) 
                    preparer.fi1l_water_tank(vehicle) 
                end
            }
        end
    end
    
    # 當引入TripCoordinator和Driver時
    class Tripcoordinator 
        def buy_food(customers)
            # ...
        end
    end
    
    class Driver 
        def gas__up(vehic1e)
            # ...
        end
        def fill_water_tank(vehicle) 
            # ...
        end
    end
    

    Trip知道太多的具體類別和方法,這裡的每一個參數都是不同的類別,並實作了不同的方法。為了知道要傳送
    哪一則訊息,你必須檢查每個參數的類別。增加case...when敘述來選擇類別,這可以解決將正確訊息傳送到
    正確物件的問題,但也會導致依賴關係暴增。

    prepare依賴於特定的類別,它依賴於那些確切的名稱。它知道每一個類別所能理解的訊息名稱,以及訊息所要
    求的參數,再次違背了迪米特法則。

  3. 引入了新的訊息,Tripprepare方法現在期望其參數為可以回應prepare, tripPreparer

    # Trip的準備工作變得更加簡單
    class Trip
        attr_reader :bicycles, :customers, :vehicle
        def prepare (preparers) 
            preparers.each {|preparer|
                preparer.prepare_trip(self) }
        end 
    end
    
    # 當所有準備者都是可以回應
    # 「prepare_trip」的鴨子類型時 
    class Mechanic 
        def prepare_trip(trip)
            trip .bicycles.each {|bicycle| prepare_bicycle(bicycle)}
        end
        # ...
    end
    
    class Tripcoordinator
        def prepare_trip(trip) 
            buy_food(trip.customers) 
        end
        # ...
    end
    
    class Driver
        def prepare_trip(trip) 
            vehicle = trip.vehicle 
            gas_up(vehicle) 
            fill_water_tank(vehicle) 
        end
        # ...
    end
    

    Preparer還沒有具體的存在。它是概念上關於公共介面的一種抽象以及協定。這是一種虛構化的設計。實作
    prepare_trip的物件就是Preparer。與之相反,那些與Preparer進行互動的物件只需要相信它們實作 Preparer介面即可。

    修改方向就會變成:prepare方法現在期望其參數為Preparer,並且每個參數的類別都實作了這個新介面。 prepare方法現在可以接受新的Preparer,同時不用被迫進行修改。如果有需要,也可以輕易地建立出其他
    更多的Preparer

    抽象程式碼最初可能很模糊,但一旦理解之後,修改會變得更加容易。使用鴨子類型會將程式碼從具體逐漸轉變為
    抽象,進而讓程式碼更易於擴展。

撰寫鴨子類型的程式碼

識別出隱藏的鴨子類型

  1. 選擇類別的case...when敘述
    有一條case...when敘述,它用於對應用程式裡的領域物件類別名進行選擇。

    class Trip
        attr_reader :bicycles, :customers, :vehicle
    
        def prepare(preparers) 
            preparers.each {|preparer|
                case preparer 
                when Mechanic
                    preparer.prepare_bicycles(bicycles)
                when Tripcoordinator
                    preparer.buy_food(customers)
                when Driver
                    preparer.gas_up(vehicle) 
                    preparer.fi1l_water_tank(vehicle) 
                end
            }
        end
    end
    
  2. kind_of?」 和 「 is_a?
    kind_of?」和「 is_a?」(兩者其實是相同的),訊息也可以對類別進行檢查,相關的用法可以再翻閱文 件。

    if preparer. kind_of? (Mechanic) 
        preparer.prepare_bicycles(bicycle)
    elsif preparer.kind_of?(Tripcoordinator) 
        preparer.buy_food(customers)
    elsif preparer.kind_of?(Driver) 
        preparer.gas_up(vehicle) 
        preparer.fi1l_water_tank(vehicle)
    end
    
  3. responds_to?
    對於鴨子類別尚未理解,用以取代「 kind_of?」 和 「 is_a?」,相關的用法可以再翻閱文件。

    if preparer. responds_to? (Mechanic) 
        preparer.prepare_bicycles(bicycle)
    elsif preparer.responds_to?(Tripcoordinator) 
        preparer.buy_food(customers)
    elsif preparer.responds_to?(Driver) 
        preparer.gas_up(vehicle) 
        preparer.fi1l_water_tank(vehicle)
    end
    

    儘管上述做法稍微減少了依賴關係的數量,但仍有許多依賴關係存在。

信任鴨子類型

鴨子類型是指那些不依賴於特定類別的物件,但能夠實作相同的公共介面。建立彼此之間互相信任的物件,專注於物件之間的公共介面,而不是具體的類別實作。物件應該只依賴於其它物件的公共介面,而非知道它們的具體類別。

記錄鴨子類型

建立鴨子類型時,必須要做好記錄並且測試它們的公共介面,最好的做法是好好撰寫測試,詳細的說明在第9章裡會有更多關於鴨子類型測試的內容。

鴨子類型之間的共用程式碼

MechanicDriverTripcoordinator它們每一個都實作了prepare_trip方法,這個方法是它們唯一的共同點。它們只共用了介面,而未共用具體的實作,在實作那些類別的時候,經常需要共用某些共同的行為。如何撰寫共用程式碼在第7章裡會詳細說明的內容。

聰明的選擇鴨子類型

儘管之前的指導原則強調不應該基於物件的具體類別來傳送訊息,但有些程式碼在實際應用中仍然使用這種方式。

舉例:

# <tt>find( :first, *args) </ttz>的便捷包装器。
# 這個方法能夠接收所有傳送給<tt>find(:first)</tt> 
# 的相同參數。
def first(*args)
  if args.any?
    if args.first.kind_of?(Integer) ||
       (loaded? && !args.first.kind_of?(Hash))
     to_a.first(*args)
    else
      apply_finder_options(args.first).first
    end 
  else
    find_first
  end 
end

程式碼依賴於較為穩定和不容易改變的Ruby核心類別(例如IntegerHash),在這種情況下,依賴於特定類別的實作可能是較為合理的,因為這樣的依賴相對安全。

上面這項範例的底層鴨子類型跨越了integerHash,因此在實作它時需要修改Ruby的基本類別。這種做法被稱為 「猴子補丁」(monkey patching),所謂猴子補丁是一種編程技巧,用拼湊程式碼的方法修改程式邏輯,可以在程式運行時動態修改或擴充程式碼。這樣的做法需要謹慎,因為它可能會對程式碼的穩定性和可維護性產生影響。

設計的目的是降低成本(請將這個衡量標準應用到所有狀況),如果建立鴨子類型會減少不穩定的依賴關係數量,就應該要這麼做。

今天一樣先瞭解到這,大家消化吸收一下,明天我們將針對 「型別」 去探討和延伸哦~

參考資料:


上一篇
Day9 CH4 建立靈活的介面(下)
下一篇
Day11 CH6 使用鴨子類型技巧降低成本(下)
系列文
入坑 RoR 必讀 - Ruby 物件導向設計實踐30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言