封裝與依賴隔離

更新於 發佈於 閱讀時間約 7 分鐘

物件導向設計的一個重點就是封裝,這有很多層面上的意義,但基本上就是控制物件的成員變數和方法的存取權。物件導向的封裝還跟繼承機制有關,這使得有一些時候我們逼不得已必須把函式定義在類別上,這種做法使得物件的功能變得難以拆解。封裝應該是模組的職責,並不需要再給物件相同的能力。


一般的模組系統就是把相關的程式碼蒐集起來放在一起,並根據粒度組織成階層結構,越內層的模組越細緻複雜且功能越多。一般會根據任務把相關的程式碼組成一個模組,例如連線相關的模組、數值計算相關的模組、資料庫管理相關的模組等,如此要找程式碼時就比較容易。更細緻的模組則會作為子模組放在相應的模組裡,例如連線相關的模組可能包含socket的模組、http protocol的模組等。這種透過階層結構管理程式碼的方式就是模組化設計。模組化設計透過組織相關聯的程式碼將知識連結起來,讓它們形成一個概括的概念,並透過模組的階層關係把更複雜的結構分解成較小的相對分離的部分,使得知識更容易學習與理解。物件導向把這種模組化的思維應用在物件上,與此物件所表達的概念相關的操作就被實作成它的方法。這也使得物件常常被用做功能導向的管理器,例如ConnectionManager。相對地,trait/typeclass只有在編譯機制上把函式與類型關聯起來,它們的實作仍可以擺在任何地方,唯有透過模組系統,才能有效的把相關的程式碼組織起來。


模組還有控制存取權的功能,也就是決定誰可以存取哪個函式或是類型,這讓它具有封裝的能力。透過存取權的控制,我們可以保證某些不安全的函式或成員變數只能被特定位置的函式使用,因此讓一些非法的情境實質上不可能發生。例如我們必須限制類別class Interval { float min; float max; }的成員的存取,因為它們必須符合min <= max的限制,應提供其他方法安全地存取它們。這種方式可以用來保證資料的完整性,它相較於利用代數資料類型更具彈性,事實上物件導向傾向使用這種方式而非代數資料類型,這可以稱作是對資料完整性的封裝。這種存取權的控制可以讓一些較複雜的操作不會暴露在外面,畢竟它們不能被存取,當我們在外面時就不需要理解它在幹嘛,因而做到對複雜度的封裝。它也能用來控制函式之間的依賴關係,讓內部的功能強大且情境複雜的函式不會被外部使用,從而降低誤用函式的風險,並減少修改此函式所需考慮的問題。而最外層的存取權一般用作函式庫的介面,它不能隨意更改,否則會產生未知的影響範圍。


模組控制存取權的方法是藉由定義一個包括函式、類型、介面等實體的範圍,並選擇要公開哪個實體,範圍外部的實體只能使用已公開的實體。這種控制存取權的功能與資訊安全無關,它是用來控制實體間的依賴關係,只要一個實體被公開,它就有可能被外部依賴,因此我們不能隨意修改這個實體的承諾。反之,透過限制可能的依賴關係與範圍,可以讓我們減低一些顧慮。同樣的概念也可以應用到函式裡,我們可以藉由限制變數的作用域,防止變數被其他地方的程式碼誤用,並讓我們可以專注在較小的範圍裡。例如當我們需要較複雜的方法準備一個值傳給函式時,與其寫成let a = …; let b = …; let param = prepare(a, b);,不如把a, b限制在範圍內:let param; { let a = …; let b = …; param = prepare(a, b); },如果是rust則可以寫成let param = { let a = …; let b = …; prepare(a, b) }以避免變數修改。我們也可以藉由直接抽取成函式做到一樣的結果,但它還需要將範圍內所使用的外部變數捕捉起來,並把需要輸出的變數回傳到範圍外。


有些人建議應該要把複雜函式的每個步驟都拆解成多個小函式以減少縮排,但這樣做會讓每個小函式都暴露在外面,如果它所在的模組或類型很大的話,就會造成一定程度的困擾。這這種基於實作且偏向特定情境的函式本來就應該限制在非常小的作用域裡,就像上面的準備參數值的例子一樣。我把這種存取範圍超出預期的狀況稱為「依賴洩漏」,定義函式時應該在意函式可以被誰存取,這會決定函式的重要性與使用情境,不合適的存取範圍會讓實體之間的依賴關係被污染。相反地,我們可以藉由「依賴隔離」解決這個問題。我們可以藉由模組的存取權控制把小函式限縮在一個範圍,並只開放組合起來的函式給外部使用。然而這只適用於像是rust的mod或是ts的namespace這類的小範圍模組系統,否則像是python這種依賴於檔案系統的模組系統,這種做法就必須把這個函式的定義獨立成一個檔案,這非常冗余。另外一種方法是藉由定義區域函式以限制它的作用域,也就是直接把小函式放在被分解的函式的定義範圍裡,這同樣可以達到隔離依賴範圍的目的。在Haskell這種特別依賴高階函式的程式語言中,很常會需要定義輔助函式作為參數,因此它特別提供where語句定義區域函式。更廣義的,在設計介面/特性時也應該要考慮方法之間的依賴關係,如果它包含太多方法,就會造成依賴此介面的實體依賴太多不必要的方法,因此應該盡量把方法分離成較小的介面,這稱做「介面隔離」。Iterator pattern或是pipeline pattern本質上也受惠於依賴隔離,它們透過把計算過程分解成各個簡單且獨立的部分,讓整體的邏輯變得容易理解。如果用迴圈寫一樣的邏輯,會發現當我們在閱讀時必須時刻記住這個變數是什麼意思,因為後面的程式碼有可能會用到它,而Iterator pattern把這類的依賴關係隔離開來,讓我們更能專注在每一步操作本身的意義。


控制存取權一般有兩種形式,一個是在定義函式、類型等實體時宣告是否公開,另一種則是統一在一個地方羅列需要公開的實體。像是物件導向的public/private、rust的pub、TypeScript的export就是第一種形式,而c++的標頭檔、Haskell的export list和ocaml的sig/mli則是第二種形式。第一種形式比較容易閱讀理解,而第二種讓我們必須把同樣的實體寫兩遍並放在不同地方,如果沒有language server輔助會很麻煩,而且會讓判斷實體的依賴範圍變得不直接。模組裡函式、類型的存取範圍控制應該寫在宣告附近,不同的範圍會影響它的意義與重要性,因而改變我們對這些實體的態度。


在物件導向裡,類別的封裝除了把存取控制邊界限制在物件本身,還包含一種特殊的控制方式。它可以宣告方法為protected,讓外部實體無法存取,但繼承此類別的子類別可以存取。這讓類別具有感染性,如果一個函式需要使用這個方法就只能加到這個類別上。這種特殊的控制權限非常微妙,一個方法是否該宣告成protected很難判斷,你怎麼知道只有它的子類會用到這個方法,就算是真的有必要這麼做嗎。如果你把一些關鍵方法宣告成protected,可能會造成未來在實作一些函式時也必須塞到這個類別裡面,因而讓這個類別過度膨脹,破壞單一職責的原則。事實上宣告成protected的目的常常是為了保證資料完整性,讓外部實體無法隨意修改資料或使用危險的方法,但又希望它能被子類使用或改寫。最好的方法應該是把它實作成函式,並在一個模組內就把問題處理掉並封裝起來。

留言
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
孩子寫功課時瞇眼?小心近視!這款喜光全光譜TIONE⁺光健康智慧檯燈,獲眼科院長推薦,網路好評不斷!全光譜LED、180cm大照明範圍、5段亮度及色溫調整、350度萬向旋轉,讓孩子學習更舒適、保護眼睛!
Thumbnail
孩子寫功課時瞇眼?小心近視!這款喜光全光譜TIONE⁺光健康智慧檯燈,獲眼科院長推薦,網路好評不斷!全光譜LED、180cm大照明範圍、5段亮度及色溫調整、350度萬向旋轉,讓孩子學習更舒適、保護眼睛!
Thumbnail
創作者營運專員/經理(Operations Specialist/Manager)將負責對平台成長及收入至關重要的 Partnership 夥伴創作者開發及營運。你將發揮對知識與內容變現、影響力變現的精準判斷力,找到你心中的潛力新星或有聲量的中大型創作者加入 vocus。
Thumbnail
創作者營運專員/經理(Operations Specialist/Manager)將負責對平台成長及收入至關重要的 Partnership 夥伴創作者開發及營運。你將發揮對知識與內容變現、影響力變現的精準判斷力,找到你心中的潛力新星或有聲量的中大型創作者加入 vocus。
Thumbnail
※ OPP第一大核心-封裝 封裝的精神在於將「方法」、「屬性」和「邏輯」包裝在類別裡面,透過類別的實例來實現。這樣外部物件不需要了解內部的實現細節,只需要知道如何使用該類別提供的接口即可。換句話說,封裝是將內部細節隱藏起來,只暴露必要的部分給使用者。 封裝的核心概念是,使用者如果想要接觸資料,只
Thumbnail
※ OPP第一大核心-封裝 封裝的精神在於將「方法」、「屬性」和「邏輯」包裝在類別裡面,透過類別的實例來實現。這樣外部物件不需要了解內部的實現細節,只需要知道如何使用該類別提供的接口即可。換句話說,封裝是將內部細節隱藏起來,只暴露必要的部分給使用者。 封裝的核心概念是,使用者如果想要接觸資料,只
Thumbnail
本章節的目的是讓讀者瞭解C#的物件導向特性,包括類別、繼承、多型、封裝等基本概念,以及介面、抽象類別、靜態類別等進階主題。此外,本章節也將介紹如何使用列舉、委派、Lambda表達式、泛型及反射,這些都是C#中常見的強大功能。
Thumbnail
本章節的目的是讓讀者瞭解C#的物件導向特性,包括類別、繼承、多型、封裝等基本概念,以及介面、抽象類別、靜態類別等進階主題。此外,本章節也將介紹如何使用列舉、委派、Lambda表達式、泛型及反射,這些都是C#中常見的強大功能。
Thumbnail
這系列是我在 2023 六角學院 Vue作品實戰班的筆記,筆記以本人理解的方式記錄。此篇主題為 Slot Props 進階應用 ,其中包含單筆資料、多筆資料。
Thumbnail
這系列是我在 2023 六角學院 Vue作品實戰班的筆記,筆記以本人理解的方式記錄。此篇主題為 Slot Props 進階應用 ,其中包含單筆資料、多筆資料。
Thumbnail
觀察者模式透過主題訂閱/訊息通知的機制,極度增強系統的可擴展性、靈活性以及降低組件間的耦合度。概念直觀簡單,是非常實用的設計模式。
Thumbnail
觀察者模式透過主題訂閱/訊息通知的機制,極度增強系統的可擴展性、靈活性以及降低組件間的耦合度。概念直觀簡單,是非常實用的設計模式。
Thumbnail
CSS 的繼承性是開發網頁樣式時的一個重要概念,它使得樣式設計更加靈活和高效,有助於提高程式碼的可讀性、一致性和可重用性,並加快開發速度,從而提供更好的開發體驗。
Thumbnail
CSS 的繼承性是開發網頁樣式時的一個重要概念,它使得樣式設計更加靈活和高效,有助於提高程式碼的可讀性、一致性和可重用性,並加快開發速度,從而提供更好的開發體驗。
Thumbnail
代理模式通過封裝原始對象來實現對該對象的控制和管理,同時不改變原始對象的行為或客戶端與該對象互動的方式,以此介入或增強對該對象的訪問和操作。
Thumbnail
代理模式通過封裝原始對象來實現對該對象的控制和管理,同時不改變原始對象的行為或客戶端與該對象互動的方式,以此介入或增強對該對象的訪問和操作。
Thumbnail
進入物件導向設計的實戰階段,我們通過建立人力資源管理功能來實踐理論知識。透過這些實作練習,能夠深化對物件導向概念的理解,並學會如何在實際開發中應用這些概念。
Thumbnail
進入物件導向設計的實戰階段,我們通過建立人力資源管理功能來實踐理論知識。透過這些實作練習,能夠深化對物件導向概念的理解,並學會如何在實際開發中應用這些概念。
Thumbnail
在物件導向程式設計的進階階段,學生將學習繼承、介面、抽象類別等核心概念。繼承允許類別共享屬性和方法,介面確保實現類別提供特定的方法實現,而抽象類別定義了基本結構供子類別擴展。這些知識點有助於提升程式碼的重用性、擴展性和維護性。
Thumbnail
在物件導向程式設計的進階階段,學生將學習繼承、介面、抽象類別等核心概念。繼承允許類別共享屬性和方法,介面確保實現類別提供特定的方法實現,而抽象類別定義了基本結構供子類別擴展。這些知識點有助於提升程式碼的重用性、擴展性和維護性。
Thumbnail
本階段深掘PHP中類別與物件的應用,從基本定義到屬性與方法的運用,並特別著重於訪問控制和靜態成員的概念。學生將學會如何有效地利用公開、保護、私有屬性,以及如何在不實例化的情況下透過類別名稱直接訪問靜態屬性和方法,進一步鞏固物件導向程式設計的核心知識。
Thumbnail
本階段深掘PHP中類別與物件的應用,從基本定義到屬性與方法的運用,並特別著重於訪問控制和靜態成員的概念。學生將學會如何有效地利用公開、保護、私有屬性,以及如何在不實例化的情況下透過類別名稱直接訪問靜態屬性和方法,進一步鞏固物件導向程式設計的核心知識。
Thumbnail
先前學到自定函式的使用方法,那如果在一個很龐大的程式架構中發散了一推自定函式,有沒有辦法可以整理一下,讓程式結構整齊又簡潔呢? 可以使用裝飾器staticmethod 定義靜態方法,全部整理到一個類別去,想像成是一個工具箱的概念,工具箱就是類別,靜態方法就像是裡面的工具一樣。
Thumbnail
先前學到自定函式的使用方法,那如果在一個很龐大的程式架構中發散了一推自定函式,有沒有辦法可以整理一下,讓程式結構整齊又簡潔呢? 可以使用裝飾器staticmethod 定義靜態方法,全部整理到一個類別去,想像成是一個工具箱的概念,工具箱就是類別,靜態方法就像是裡面的工具一樣。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News