少用繼承,多用介面

閱讀時間約 6 分鐘

承接上一篇文章,現代的物件導向已經走偏了,他就像null pointer,很容易出現不好的設計。自從我深入學習函數式編程後,漸漸發現物件導向的不合理的設計,而學習rust之後更讓我開始討厭物件導向,rust幾乎把所有我認為不好的地方都修正了。這個系列的文章我將會一一比較物件導向與rust的差異。這篇文章將關注於繼承機制。


物件導向的設計方式通常是先預想多個任務或情境,並設計一個物件來統一處理它,之後若是有新的情境,可以藉由繼承來延伸功能。這種設計方式好處是非常直觀且容易擴展,但這反而容易累積各種技術債。設計物件時一般都是以可擴展為前提,但我們往往無法預測它會怎麼擴展,有可能之後的擴展打破了一些原本的假設,使得之前寫的程式邏輯出現錯誤。這種「向後的抽象化設計」很容易出現這種情況,因為我們總是以「這部分的程式碼可以重用」來實作父類別,但並沒有考慮到應該在怎樣的情況下才能使用。正確的設計方式應該要寫明繼承這個類別應該符合哪些規則,也就是關注約束(constraint)而非延展(extension)。抽象化就是對未知的事物建立模型,這個過程很容易會將先入為主的假設帶入其中,而關注約束更容易發現那些未察覺的潛在資訊(unknown knowns),讓你更能夠意識到你的程式邏輯運用了哪些假設。


這種關注約束的設計方式更適合使用介面而非類別來定義規則,尤其是當你想要抽象化的行為不侷限於一種類別時。例如,當你想要設計一個可以連線的物件,但又不想限制在某種情境下運作,你應該設計一個介面Connectable而非抽象類別Connection。介面一般不帶有預設實作,因此本身並不具有重用程式碼的能力。若是要重用程式碼,不應依賴於繼承機制,而是使用高階函式或是組合物件等方法,如此更容易把規則理清。除去延伸功能後,繼承仍可以用來細分類別,也就是讓物件在已知的情境下做出不同的行為,而這個機制應當只在內部使用,不應讓它在你不知道的地方被繼承改寫。


rust的trait system類似於介面,但並不完全一樣。trait(特性)跟介面一樣都是用於描述行為,也一樣都是關注於約束而非延展。實作介面一般需要在定義類別時實作介面的方法,也就是介面是類別的一部分。而trait描述的是特性本身,並不需要在定義類別時實作。trait所描述的特性並不侷限於單個類別,它也可以描述多個類別之間的特性,例如A: Add<B>描述的是A與B之間的加法關係。rust甚至可以為符合某些條件的所有類別實作trait,例如我們可以為所有擁有特性A: Add<A> 的類別A實作A: Mul<uint>。基於以上能力,我們不應該把trait當作類別的一部分,而是特性本身。然而rust把trait的語法設計的跟介面一樣,其實它應該寫作Add<A,B>,也就是「存在一個trait描述A與B之間的加法特性」,編譯器會根據上下文去搜尋合適的trait。trait 的這種設計把類別的定義與trait的定義分開,讓我們可以把不同的關注點分離開來,而非像介面必須綁在類別的定義之上。


rust跟物件導向的另一個很大的不同是rust沒有類似於繼承的子類關係,但是有基於泛型的子類關係。在有繼承特性的程式語言中,當你拿到了一個類別為A的參數,它可能其實是B的物件實體,只是它的類別繼承自A。這種子類關係使得類別無法描述實際的物件實體,迫使我們必須以抽象的方式思考程式邏輯,而非基於實作細節。這種隱藏部分細節的做法讓我們能夠把雜音去除,讓我們專注於更重要的概念與程式邏輯。然而我們沒辦法限制變數只能是哪種實體,有些程式語言甚至沒辦法阻止類別被繼承,就算能也不適用於所有情況,這使得我們需要額外注意上下文才能判斷變數到底是不是延展過的物件。


基於泛型的子類關係則是利用類型參數與類型約束界定類別的範圍,例如List<A> where A: Comparable 代表由所有內容可比較的列表的集合,也就是List<int>, List<string>, List<List<int>> 等類別的聯集(正確來說是indexed family)。他的子類關係建立在約束的強度,例如它是 List<A> where A: Equal 的子類,因為所有可比較的類型都是可以判斷相等的,因此其組成的列表也有子類關係。類型參數明確地表明我們不知道它到底是什麼,只知道它有什麼特性,因此更能區別抽象與實作的邊界。你可以把類型參數想像成某個類別「未知」的部分,比起宣告x: List<Comparable>(變數x是內容可以比較的列表,但我不知道他的內容到底是什麼),不如宣告A: Comparable跟x: List<A>(變數x是內容可以比較的列表,但我不知道他的內容到底是什麼,姑且叫他A)。透過泛型,我們可以把未知的部分串聯起來,並使程式邏輯更加一致。


rust的trait在意義上跟類型有很大的不同,他更像是類型的種類而非一種特殊的類型,因此不能直接宣告變數為 Comparable,必須藉由類型參數的約束描述物件的抽象行為。因此trait用來描述抽象的概念,而型別則用於描述實際的資料結構。這一點跟介面很不一樣,我們一般都會把介面當作一種純抽象類別,因而用類似繼承的概念思考。在trait system中,A: Comparable代表的不是「類型A是類型Comparable的子類」,而是類型本身是Comparable裡的一個類別。Comparable可以看作是類別的集合,而我們透過這些集合界定我們想要的類型,就像是我們可以透過類型界定我們想要的物件,因此trait被稱為二階類型(2-types, kinds)。正因為它們不是包含關係(⊆)而是隸屬關係(∈),抽象的概念才能完全從類別獨立出來。

3會員
23內容數
這不是教你如何從物件導向到函數式編程的入門教程。我會深入探討物件導向與函數式編程的差異,並討論為什麼你應該使用函數式編程並徹底放棄物件導向。
留言0
查看全部
發表第一個留言支持創作者!