進一步思考Duck typing


iThome 網站首載:進一步思考Duck typing

由於變數本身不帶型態資訊,動態定型語言在設計方法時,想要不受限於物件型態而僅思考物件應當具有的行為,也就是進行所謂Duck typing時極為容易,然而Duck typing並非動態定型語言專屬,靜態定型語言也能夠進行Duck typing,只不過在方式上通常較動態定型麻煩,既然動態與靜態定型都有能力進行Duck typing,那麼進一步思考兩者間的差異,才能真正發揮與掌握Duck typing。

你只要像個鴨子呱呱叫

支持者最愛擁抱動態定型語言的理由之一就是,實作Duck typing時極為容易,也就是「當我看到一隻鳥,它走路像鴨子,游泳像鴨子,叫聲像鴨子,我就稱其為鴨子」此說法最初起源可能來自美國印第安納詩人James Whitcomb Riley,用來表示可觀察未知事物的外在特徵,以推斷該事物的本質,後來在程式設計也常用來隱喻動態定型語言的設計風格,若使用動態定型的Ruby語言來示範,可定義一個doQuack方法:

def doQuack(duck)
    duck.quack
end

參數duck沒有任何型態宣告,也因此,不會有任何型態約束,只要傳進來的物件具有quack方法就可以了,在快速開發、程式規範尚未確立的階段若有新需求,只要在doQuack方法中修改,不用費心地修改類別等定義,這個特性是非常有用的功能,如果語言本身又有支持物件個體化(Object individuation),即使物件原本沒有quack方法,也可以臨時添加再做為引數傳入。例如,Ruby可以臨時為物件定義單例方法(singleton method ),就能滿足呼叫doQuack的需求:

dog = Dog.new
def dog.quack
    print "quack"
end
doQuack(dog)

靜態定型語言也能Duck typing

常見對靜態定型語言的誤解是,它們沒辦法進行Duck typing,實際上並非如此,身為靜態定型的Scala,可以藉由Structural typing語法,在參數上宣告傳入的物件必須具備何種行為,例如,def doQuack(duck: {def quack: Unit}),如此,任何具有quack行為的物件,都能呼叫doQuack方法,而不具quack行為的物件試圖傳入,就會引發編譯錯誤,也就是又多了個編譯時期檢查的優點。

不過也因為這樣而有人說,Scala的Structural typing只能算是靜態時期的Duck typing,動態定型語言的Duck typing實際上發生於執行時期,這多了更多彈性,然而,在Java這門靜態定型語言中,透過反射(Reflection)機制,也能實作出具備動態時期Duck typing的功能,不過,這不是Java語法上的直接支援,而是透過程式庫來達成,因而就實作本身來說,程式碼囉嗦了許多。

靜態定型語言本身就對型態多所約束,像Java在實作Duck typing時較為囉嗦,多半也表示了這門語言,不太希望你使用Duck typing,Scala雖稍微放寬了限制,提供了Structural typing語法,不過仍要求你符合型態約束,{def quack: Unit}實際上就是在定義一個匿名的型態,這個型態下規範了呼叫函式時,物件本身應當具備哪些行為子集。

Duck typing是在思考一組行為集合

實際上,在動態定型語言中使用Duck typing時,也是在使用一組行為集合,也就是你在方法中對該物件進行的操作集合,傳入的物件會有許多行為,但如果行為中沒有方法要求的子集合,就會引發執行時期錯誤,Scala的Structural typing語法,其實是在協助將這種執行時期錯誤推向編譯時期就檢查出來,若是使用Java的反射機制,不希望引發執行時期錯誤,就得在執行時期進行檢查,確認物件是否有要求的行為子集。

在程式開發領域,Alex Martelli在2000年於comp.lang.python新聞群組就使用了Duck typing這個名詞,他談到「別檢查它是不是一隻鴨子,要檢查它是不是像隻鴨子呱呱叫,像隻鴨子走路等,取決於你需要哪些鴨子似的行為來操弄語言」,以先前Ruby版本的doQack方法為例,你採用了Duck typing,若需要對傳入物件進行檢查時,不是使用is_a?方法來檢查傳入物件是不是Duck型態的實例,而要使用respond_to?來檢查是不是具有quack行為,畢竟你採用了Duck typing,檢查時也要Duck typing,在JavaScript領域常見對物件進行特性偵測的技巧,也是同樣的道理。

Duck typing是在思考一組行為集合,有時也要再思考一下,這組行為應當屬於誰?是否該屬於某一個類別的實例?舉例來說,如果你為了滿足doQuack方法,曾多次在程式中如先前對某類別實例臨時添加方法後傳入,實際上Ruby中對實例定義單例方法,就開啟了該實例的匿名單例類別(Anonymous singleton class),也就是你還是在屬於該實例的類別上定義了方法,只是該類別是匿名的,如果你經常做這個動作,就該想想是不是要使用開放類別(Open class)語法將或者是直接將quack方法定義在生成該實例的類別之上。

如果Duck typing的這組行為,是可以由不同類別的實例來共享,那麼應該思考一下,這組行為是不是可以抽出為模組(module)重用,並在需要的類別定義中包括(include)該模組,實際上,在JDK8之後interface有了預設方法(Default method),就是在解決這類需求,即使在JDK8前interface只能定義公開的行為規範,其目的也是在要求開發者思考,哪些行為集合需要共用,而這組行為是不是該取個明確的名稱,並要求開發者共同遵守與實作。

你真的想要狗呱呱叫嗎?

在Java這門靜態定型語言中,有著許多的強制性,因而令Java顯得囉嗦,然而在某些場合,囉嗦反而會是種優點,如果某組行為應當屬於某個類別,而方法的參數宣告為該類別,那麼不是該類別的實例,就不能傳入方法,也許你並不是真的想要一隻狗傳入doQuack方法;動態定型語言中,設計API的開發者也許知道不該傳入一隻狗給doQuack,然後API(特別是公用API)客戶端不見得知道或願意遵守這種慣例約束,若是如此,進行Duck typing的行為檢查就是必要的。

也許你真的想要一隻狗呱呱叫,也可以允許一隻貓喵喵叫,但就是不允許一隻龍呱呱叫,像這類情況,Java中可以定義DuckLike介面,讓狗與貓的類別來實作DuckLike介面,這樣只要在doQuack方法上宣告DuckLike,只要龍沒有實作DuckLike介面,編譯器就會在執行前抱怨有隻龍不想遵守規範,在開發成員眾多、水準不一、慣例之類的約束難以貫徹或確實傳達的環境中,這種強制規範算是種好處。

無論是靜態定型、動態定型或是Duck typing,都得進行定型,也就是有哪些相關的值或行為能夠集合在一起成為一個型態,無論那型態是否具有名稱,沒有了型態宣告,沒有了編譯器在程式執行前的型態約束檢查,並不代表你不會遇到定型的問題,只不過是將問題推到執行時期罷了,在具有更大彈性的動態定型或動態語言中,你擁有更多的選擇性,而不是被編譯器之類的工具直接限制住,就因為擁有更多的選擇性,對於任何像Duck typing這類具強大威力的特性,都得做更進一步思考,才能瞭解這些強大威力的特性背後代表了哪些設計意義。