少用繼承,多用介面

更新於 發佈於 閱讀時間約 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)。正因為它們不是包含關係(⊆)而是隸屬關係(∈),抽象的概念才能完全從類別獨立出來。

留言
avatar-img
留言分享你的想法!
avatar-img
have bear的沙龍
4會員
28內容數
這不是教你如何從物件導向到函數式編程的入門教程。我會深入探討物件導向與函數式編程的差異,並討論為什麼你應該使用函數式編程並徹底放棄物件導向。
have bear的沙龍的其他內容
2024/02/15
這個系列的文章主要專注於物件導向到函數式編程的差異與分析,並針對概念與機制上的不同進行比較。很多人說物件導向和函數式編程沒有哪個比較好的問題,只有哪個比較適合的問題,然而我並不這麼認為,我透過這一系列的文章從各個角度討論它們之間的優缺點就是為了闡述我的觀點。物件導向錯在沒有理論基礎,但它贏在熟悉性,
2024/02/15
這個系列的文章主要專注於物件導向到函數式編程的差異與分析,並針對概念與機制上的不同進行比較。很多人說物件導向和函數式編程沒有哪個比較好的問題,只有哪個比較適合的問題,然而我並不這麼認為,我透過這一系列的文章從各個角度討論它們之間的優缺點就是為了闡述我的觀點。物件導向錯在沒有理論基礎,但它贏在熟悉性,
2024/02/08
前一篇文章中所提的函數式的三個機制明確說明了它關注的規則與能力具體是什麼。然而這套對於函數式編程的定義主要基於特定的類型系統,作為一個編程範式來說過於狹隘(物件導向的定義也是這樣)。更廣義的,我認為函數式編程主要依循三個原則,它們可以應用於任何程式語言,就算沒有靜態類型系統的支援也可以。例如在不管類
2024/02/08
前一篇文章中所提的函數式的三個機制明確說明了它關注的規則與能力具體是什麼。然而這套對於函數式編程的定義主要基於特定的類型系統,作為一個編程範式來說過於狹隘(物件導向的定義也是這樣)。更廣義的,我認為函數式編程主要依循三個原則,它們可以應用於任何程式語言,就算沒有靜態類型系統的支援也可以。例如在不管類
2024/02/04
前面談了那麽多函數式編程與物件導向的差異,但我們還沒定義函數式編程。就像物件導向,函數式編程沒有明確的定義,每個人對於什麼是函數式編程都有不同的看法。在這裡我會總結前面的討論,給出我對於函數式編程的觀點。 物件導向注重封裝與延展性,因此一般基於三個機制:繼承、多型、封裝,它們代表了物件導向所重
2024/02/04
前面談了那麽多函數式編程與物件導向的差異,但我們還沒定義函數式編程。就像物件導向,函數式編程沒有明確的定義,每個人對於什麼是函數式編程都有不同的看法。在這裡我會總結前面的討論,給出我對於函數式編程的觀點。 物件導向注重封裝與延展性,因此一般基於三個機制:繼承、多型、封裝,它們代表了物件導向所重
看更多
你可能也想看
Thumbnail
常常被朋友問「哪裡買的?」嗎?透過蝦皮分潤計畫,把日常購物的分享多加一個步驟,就能轉換成現金回饋。門檻低、申請簡單,特別適合學生與上班族,讓零碎時間也能創造小確幸。
Thumbnail
常常被朋友問「哪裡買的?」嗎?透過蝦皮分潤計畫,把日常購物的分享多加一個步驟,就能轉換成現金回饋。門檻低、申請簡單,特別適合學生與上班族,讓零碎時間也能創造小確幸。
Thumbnail
嗨!歡迎來到 vocus vocus 方格子是台灣最大的內容創作與知識變現平台,並且計畫持續拓展東南亞等等國際市場。我們致力於打造讓創作者能夠自由發表、累積影響力並獲得實質收益的創作生態圈!「創作至上」是我們的核心價值,我們致力於透過平台功能與服務,賦予創作者更多的可能。 vocus 平台匯聚了
Thumbnail
嗨!歡迎來到 vocus vocus 方格子是台灣最大的內容創作與知識變現平台,並且計畫持續拓展東南亞等等國際市場。我們致力於打造讓創作者能夠自由發表、累積影響力並獲得實質收益的創作生態圈!「創作至上」是我們的核心價值,我們致力於透過平台功能與服務,賦予創作者更多的可能。 vocus 平台匯聚了
Thumbnail
本章節是Java入門的第八天,主要介紹物件導向的概念。這包括了類別、建構子、存取修飾子、繼承、多型、封裝、介面、抽象類別、靜態類別、列舉、Lambda表達式、泛型和反射等主題。每個主題都配有相關的程式碼範例,以協助讀者更好地理解這些概念。
Thumbnail
本章節是Java入門的第八天,主要介紹物件導向的概念。這包括了類別、建構子、存取修飾子、繼承、多型、封裝、介面、抽象類別、靜態類別、列舉、Lambda表達式、泛型和反射等主題。每個主題都配有相關的程式碼範例,以協助讀者更好地理解這些概念。
Thumbnail
本章節的目的是介紹 Kotlin 中的物件導向概念。這包括了類別、繼承、多型、封裝、介面、抽象類別、靜態類別、列舉、委派、Lambda 表達式、泛型以及反射等概念。每一個概念都會透過範例程式碼來解釋其功能和用法。
Thumbnail
本章節的目的是介紹 Kotlin 中的物件導向概念。這包括了類別、繼承、多型、封裝、介面、抽象類別、靜態類別、列舉、委派、Lambda 表達式、泛型以及反射等概念。每一個概念都會透過範例程式碼來解釋其功能和用法。
Thumbnail
這個章節主要介紹了Swift程式語言中物件導向程式設計的基本概念,包括類別、建構子、公開、私有、受保護等等的概念。同時,也介紹了繼承、多型、封裝、介面、抽象類別、靜態類別、列舉、委派、Lambda表達式、泛型和反射等進階特性。
Thumbnail
這個章節主要介紹了Swift程式語言中物件導向程式設計的基本概念,包括類別、建構子、公開、私有、受保護等等的概念。同時,也介紹了繼承、多型、封裝、介面、抽象類別、靜態類別、列舉、委派、Lambda表達式、泛型和反射等進階特性。
Thumbnail
本章節是一個初級的 TypeScript 教學,主要介紹了 TypeScript 中物件導向程式設計的各種核心概念,包括類別、建構子、存取修飾子、繼承、多型、封裝、介面、抽象類別、靜態類別、列舉、委派、Lambda 表達式、泛型和反射等。每個概念都通過詳細的解釋和實例代碼來進行深入的介紹。
Thumbnail
本章節是一個初級的 TypeScript 教學,主要介紹了 TypeScript 中物件導向程式設計的各種核心概念,包括類別、建構子、存取修飾子、繼承、多型、封裝、介面、抽象類別、靜態類別、列舉、委派、Lambda 表達式、泛型和反射等。每個概念都通過詳細的解釋和實例代碼來進行深入的介紹。
Thumbnail
本章節旨在介紹JavaScript中的物件導向編程。內容包括類別(Class)的定義和使用,建構子的作用,以及公開,私有,受保護(Protected)等不同訪問修飾符的概念。此外,還涵蓋了繼承、多型、封裝、介面、抽象類別、靜態類別、列舉、委派、Lambda表達式、泛型、反射等物件導向的主要觀念。
Thumbnail
本章節旨在介紹JavaScript中的物件導向編程。內容包括類別(Class)的定義和使用,建構子的作用,以及公開,私有,受保護(Protected)等不同訪問修飾符的概念。此外,還涵蓋了繼承、多型、封裝、介面、抽象類別、靜態類別、列舉、委派、Lambda表達式、泛型、反射等物件導向的主要觀念。
Thumbnail
本章節的目的是讓讀者瞭解C#的物件導向特性,包括類別、繼承、多型、封裝等基本概念,以及介面、抽象類別、靜態類別等進階主題。此外,本章節也將介紹如何使用列舉、委派、Lambda表達式、泛型及反射,這些都是C#中常見的強大功能。
Thumbnail
本章節的目的是讓讀者瞭解C#的物件導向特性,包括類別、繼承、多型、封裝等基本概念,以及介面、抽象類別、靜態類別等進階主題。此外,本章節也將介紹如何使用列舉、委派、Lambda表達式、泛型及反射,這些都是C#中常見的強大功能。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News