封裝與依賴隔離

更新於 發佈於 閱讀時間約 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
4會員
28內容數
這不是教你如何從物件導向到函數式編程的入門教程。我會深入探討物件導向與函數式編程的差異,並討論為什麼你應該使用函數式編程並徹底放棄物件導向。
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
have bear的沙龍 的其他內容
上一篇文章提到有些介面不應被繼承,但物件導向的子類別只能繼承父類別的介面,因而產生一些不合適的介面實作。trait/typeclass則沒有這種繼承機制,這似乎使需要繼承的特性無法直接使用。然而函數式導向比起繼承,更適合使用組合,根本不需要使用繼承疊加特性。 資料類型的定義往往跟怎麼建構模型相
類似於trait/typeclass的特性系統能提供程式「延展性」,它能讓函式針對不同的類型做出不同的行為。這種機制與物件導向的繼承非常像,然而特性系統的彈性比較大一點,而且概念上也有一些差別。為了探討討論這些差異,我們必須深入了解繼承機制到底是什麼。 繼承並不是建立子類關係的唯一方法。所謂的
所謂的多型是讓一個函式或是資料結構能擁有多個不同的類型,其中上一篇文章所談的就是參數多型(parametric polymorphism),這篇文章將繼續討論特設多型(ad hoc polymorphism)。特設多型跟泛型的差別在於:泛型函式對於所有的類型只能有一種實作,而特設多型會根據類型有不同
前幾篇文章討論了類型系統的合理性,而這會影響我們對於變數與函式是什麼的理解。其中泛型是當中很重要的一個元素,很多討論都是基於泛型的使用。泛型會大大地增加類型系統的複雜度,因此有些語言選擇不提供泛型(go),但缺少泛型又會使簡單的容器都無法用類型精確描述。泛型的強大必須結合有紀律的類型系統才能顯現,但
函式跟資料結構一樣都有類型,它不只是特定於函式的概念,而是跟int, tuple<bool,float>等類型同等的概念。在c++函式的類型可以寫做如std::function<int(float,float)>,它可以放在tuple, array等容器裡,當然也可以作為函式的參數或是傳回值,如st
前幾篇文章在討論類型時,只討論了乘法與加法類型,這只是最基礎的類型構造方式,另外還有函式類型和泛型等概念還沒討論。在討論函式的類型之前,必須先討論函式的正確用法。對於程序式編程來說,函式是一段可重複使用的執行代碼,輸入的參數是用來控制執行行為的,因此比起函式(function)更應該稱它為程序(pr
上一篇文章提到有些介面不應被繼承,但物件導向的子類別只能繼承父類別的介面,因而產生一些不合適的介面實作。trait/typeclass則沒有這種繼承機制,這似乎使需要繼承的特性無法直接使用。然而函數式導向比起繼承,更適合使用組合,根本不需要使用繼承疊加特性。 資料類型的定義往往跟怎麼建構模型相
類似於trait/typeclass的特性系統能提供程式「延展性」,它能讓函式針對不同的類型做出不同的行為。這種機制與物件導向的繼承非常像,然而特性系統的彈性比較大一點,而且概念上也有一些差別。為了探討討論這些差異,我們必須深入了解繼承機制到底是什麼。 繼承並不是建立子類關係的唯一方法。所謂的
所謂的多型是讓一個函式或是資料結構能擁有多個不同的類型,其中上一篇文章所談的就是參數多型(parametric polymorphism),這篇文章將繼續討論特設多型(ad hoc polymorphism)。特設多型跟泛型的差別在於:泛型函式對於所有的類型只能有一種實作,而特設多型會根據類型有不同
前幾篇文章討論了類型系統的合理性,而這會影響我們對於變數與函式是什麼的理解。其中泛型是當中很重要的一個元素,很多討論都是基於泛型的使用。泛型會大大地增加類型系統的複雜度,因此有些語言選擇不提供泛型(go),但缺少泛型又會使簡單的容器都無法用類型精確描述。泛型的強大必須結合有紀律的類型系統才能顯現,但
函式跟資料結構一樣都有類型,它不只是特定於函式的概念,而是跟int, tuple<bool,float>等類型同等的概念。在c++函式的類型可以寫做如std::function<int(float,float)>,它可以放在tuple, array等容器裡,當然也可以作為函式的參數或是傳回值,如st
前幾篇文章在討論類型時,只討論了乘法與加法類型,這只是最基礎的類型構造方式,另外還有函式類型和泛型等概念還沒討論。在討論函式的類型之前,必須先討論函式的正確用法。對於程序式編程來說,函式是一段可重複使用的執行代碼,輸入的參數是用來控制執行行為的,因此比起函式(function)更應該稱它為程序(pr
你可能也想看
Google News 追蹤
Thumbnail
隨著理財資訊的普及,越來越多台灣人不再將資產侷限於台股,而是將視野拓展到國際市場。特別是美國市場,其豐富的理財選擇,讓不少人開始思考將資金配置於海外市場的可能性。 然而,要參與美國市場並不只是盲目跟隨標的這麼簡單,而是需要策略和方式,尤其對新手而言,除了選股以外還會遇到語言、開戶流程、Ap
Thumbnail
嘿,大家新年快樂~ 新年大家都在做什麼呢? 跨年夜的我趕工製作某個外包設計案,在工作告一段落時趕上倒數。 然後和兩個小孩過了一個忙亂的元旦。在深夜時刻,看到朋友傳來的解籤網站,興致勃勃熬夜體驗了一下,覺得非常好玩,或許有人玩過了,但還是想寫上來分享紀錄一下~
※ 工廠模式 定義: 工廠模式是一種實現了「工廠」概念的物件導向設計模式。它提供一個通用的工廠介面,將創建instance(實例)的程式碼交由子類別各自實現,並根據需求去動態地生成相應的物件。這種模式將物件的創建邏輯與使用邏輯分開,使程式碼更容易維護和擴展。 特點: 具有高度標準化和同質性的
※ 單例模式介紹 ※ 定義:單例模式是一種設計模式,確保一個class(類)只有一個實例,並提供一個存取它的全域存取點。無論如何取值,皆只對這個實例取值。 ※ 目的:保證一個類別只會產生一個物件,而且提供存取該物件的統一方法。 ※ 講解:單例模式確保一個類無論怎麼 new 或 get,都只能拿
Thumbnail
※ OPP第一大核心-封裝 封裝的精神在於將「方法」、「屬性」和「邏輯」包裝在類別裡面,透過類別的實例來實現。這樣外部物件不需要了解內部的實現細節,只需要知道如何使用該類別提供的接口即可。換句話說,封裝是將內部細節隱藏起來,只暴露必要的部分給使用者。 封裝的核心概念是,使用者如果想要接觸資料,只
Thumbnail
本章節的目的是讓讀者瞭解C#的物件導向特性,包括類別、繼承、多型、封裝等基本概念,以及介面、抽象類別、靜態類別等進階主題。此外,本章節也將介紹如何使用列舉、委派、Lambda表達式、泛型及反射,這些都是C#中常見的強大功能。
※ Object(物件) & Constructor Function(建構式函式) Object(物件)是什麼? 物件是一種「可以將資料、程式碼包含在其中」的資料結構。 Object(物件)的兩種創造方式: 匿名物件:直接使用"{}"。沒有特別的名字,直接從Object中繼承過來的一個物件
Thumbnail
代理模式通過封裝原始對象來實現對該對象的控制和管理,同時不改變原始對象的行為或客戶端與該對象互動的方式,以此介入或增強對該對象的訪問和操作。
Thumbnail
進入物件導向設計的實戰階段,我們通過建立人力資源管理功能來實踐理論知識。透過這些實作練習,能夠深化對物件導向概念的理解,並學會如何在實際開發中應用這些概念。
Thumbnail
在物件導向程式設計的進階階段,學生將學習繼承、介面、抽象類別等核心概念。繼承允許類別共享屬性和方法,介面確保實現類別提供特定的方法實現,而抽象類別定義了基本結構供子類別擴展。這些知識點有助於提升程式碼的重用性、擴展性和維護性。
Thumbnail
本階段深掘PHP中類別與物件的應用,從基本定義到屬性與方法的運用,並特別著重於訪問控制和靜態成員的概念。學生將學會如何有效地利用公開、保護、私有屬性,以及如何在不實例化的情況下透過類別名稱直接訪問靜態屬性和方法,進一步鞏固物件導向程式設計的核心知識。
快速釋放固定件是一種革命性的創新解決方案,重新定義了固定效率和靈活性。這篇文章將深入探討快速釋放固定件的特點、優勢和應用,從無需工具的存取面板到模塊化組裝系統。發現這些多功能固定件如何讓製造商能夠簡化生產流程、最小化停機時間,並輕鬆適應不斷變化的設計需求。
Thumbnail
隨著理財資訊的普及,越來越多台灣人不再將資產侷限於台股,而是將視野拓展到國際市場。特別是美國市場,其豐富的理財選擇,讓不少人開始思考將資金配置於海外市場的可能性。 然而,要參與美國市場並不只是盲目跟隨標的這麼簡單,而是需要策略和方式,尤其對新手而言,除了選股以外還會遇到語言、開戶流程、Ap
Thumbnail
嘿,大家新年快樂~ 新年大家都在做什麼呢? 跨年夜的我趕工製作某個外包設計案,在工作告一段落時趕上倒數。 然後和兩個小孩過了一個忙亂的元旦。在深夜時刻,看到朋友傳來的解籤網站,興致勃勃熬夜體驗了一下,覺得非常好玩,或許有人玩過了,但還是想寫上來分享紀錄一下~
※ 工廠模式 定義: 工廠模式是一種實現了「工廠」概念的物件導向設計模式。它提供一個通用的工廠介面,將創建instance(實例)的程式碼交由子類別各自實現,並根據需求去動態地生成相應的物件。這種模式將物件的創建邏輯與使用邏輯分開,使程式碼更容易維護和擴展。 特點: 具有高度標準化和同質性的
※ 單例模式介紹 ※ 定義:單例模式是一種設計模式,確保一個class(類)只有一個實例,並提供一個存取它的全域存取點。無論如何取值,皆只對這個實例取值。 ※ 目的:保證一個類別只會產生一個物件,而且提供存取該物件的統一方法。 ※ 講解:單例模式確保一個類無論怎麼 new 或 get,都只能拿
Thumbnail
※ OPP第一大核心-封裝 封裝的精神在於將「方法」、「屬性」和「邏輯」包裝在類別裡面,透過類別的實例來實現。這樣外部物件不需要了解內部的實現細節,只需要知道如何使用該類別提供的接口即可。換句話說,封裝是將內部細節隱藏起來,只暴露必要的部分給使用者。 封裝的核心概念是,使用者如果想要接觸資料,只
Thumbnail
本章節的目的是讓讀者瞭解C#的物件導向特性,包括類別、繼承、多型、封裝等基本概念,以及介面、抽象類別、靜態類別等進階主題。此外,本章節也將介紹如何使用列舉、委派、Lambda表達式、泛型及反射,這些都是C#中常見的強大功能。
※ Object(物件) & Constructor Function(建構式函式) Object(物件)是什麼? 物件是一種「可以將資料、程式碼包含在其中」的資料結構。 Object(物件)的兩種創造方式: 匿名物件:直接使用"{}"。沒有特別的名字,直接從Object中繼承過來的一個物件
Thumbnail
代理模式通過封裝原始對象來實現對該對象的控制和管理,同時不改變原始對象的行為或客戶端與該對象互動的方式,以此介入或增強對該對象的訪問和操作。
Thumbnail
進入物件導向設計的實戰階段,我們通過建立人力資源管理功能來實踐理論知識。透過這些實作練習,能夠深化對物件導向概念的理解,並學會如何在實際開發中應用這些概念。
Thumbnail
在物件導向程式設計的進階階段,學生將學習繼承、介面、抽象類別等核心概念。繼承允許類別共享屬性和方法,介面確保實現類別提供特定的方法實現,而抽象類別定義了基本結構供子類別擴展。這些知識點有助於提升程式碼的重用性、擴展性和維護性。
Thumbnail
本階段深掘PHP中類別與物件的應用,從基本定義到屬性與方法的運用,並特別著重於訪問控制和靜態成員的概念。學生將學會如何有效地利用公開、保護、私有屬性,以及如何在不實例化的情況下透過類別名稱直接訪問靜態屬性和方法,進一步鞏固物件導向程式設計的核心知識。
快速釋放固定件是一種革命性的創新解決方案,重新定義了固定效率和靈活性。這篇文章將深入探討快速釋放固定件的特點、優勢和應用,從無需工具的存取面板到模塊化組裝系統。發現這些多功能固定件如何讓製造商能夠簡化生產流程、最小化停機時間,並輕鬆適應不斷變化的設計需求。