閒談軟體設計:Immutable Interface

閱讀時間約 11 分鐘
這是很久前的舊文, 不過,回頭再看一遍,還是挺有意思的。順便更新一下,Java 9 針對 library 開發有更好的存取限制 (模組),可將某些類別和函式限定成同模組就可以存取,這很適合將實際實作的類別隱藏起來,但外部是看不到的。Swift 也有 privatefileprivateinternal 和 public 等存取限制,確實方便很多,但還是會有需要 Immutable Interface 的時候。

引子

先前和剛開始寫 iOS 的同事 pair programming,同事對於我習慣將 private member data 宣告成 read-only property 感到有點疑惑,老實說,這習慣是只有寫 Objective C 時才有的,而且不是全部的 private member data 都是如此,主因是 Objective C 對於存取限制的設計不像 C++ / Java / C# 那樣顯著,但這確實讓我時常思考:存取限制的目的是什麼?

像是 JavaScript 存取限制的設計也不明顯,最後還是回到 OO 的基本特性之一:封裝。封裝有時會被誤以為是 information hiding,我認為封裝最大的用意是讓使用者以既定規範使用被封裝的元件,避免元件被破壞。因此,在不違反這原則下,我能接受部分資訊以唯讀的方式暴露出來,有時唯讀資訊還能讓測試變容易。

另外還有一個要思考的是,除了語言本身提供對存取限制的支援外,沒有其他方法能達到同樣的效果嗎?事實上,我見過很多 Java 工程師宣告 private member data 後就立即撰寫對應的 public getter 和 setter,甚至還因為覺得寫 getter/setter 很麻煩有了像 lombok 這樣的 third-party library,如果是這樣,一開始就把該 member data 設計成 public 不是更簡單?

可是有時候確實會面臨一個兩難,某個 member data 確實是可以改的,但又不是任何人都可以改,所以若不開放 setter 會變成所有人都無法修改,但開放後又變成所有人都能改,因此像 Java 提供了 protected 和 package 層級的限制,前者讓繼承的物件能夠修改,後者讓屬於同個 package 的物件能夠修改,但有時候 A 物件和 B 物件既非繼承關係也非同 package 關係,此時這些限制都無法滿足一個需求:讓物件暫時變成 immutable (Java官方有建議的方式設計永久性immutable object,但不是我要的)。

Immutable interface

一個簡單的例子,在一個行動裝置的 App 上,可能會使用一個 User 類別記錄使用者的暱稱、ID 以及照片,除了 ID 是一創建後無法修改外,其他像暱稱或是照片都是可以修改的,因此,通常會替類別加上對應的 setter,但這會有個問題,修改暱稱或是照片可能需要和 server 同步及儲存,但持有 User 物件的對象 (例如:UI),可能誤以為呼叫對應的 setter 即可完成任務,確實可以在 setter 做完這些事情,但這會讓 User 的responsibility 變繁重,或是透過 observer 的方式,當任何屬性有變更時,通知 observer 做對應的動作 (和 server 同步及儲存),但若 observer 發生錯誤,處理錯誤變得很不乾脆,所以這兩種方式我都沒使用過。

為了不讓不合適的物件持有者修改物件狀態,可以將 getter 和 setter 分離,將 getter 全部集中到介面上,一個只有 getter 的 interface,稱為 Immutable interface,setter 則保留在實作的類別上,如Figure 1,UserInfo 僅宣告getter,而實作的 User 依舊提供 setter。當 UI 需要呈現使用者的暱稱和照片時,可以只傳遞 UserInfo 給 UI,雖然 UI 取得的是只有 getter 的物件,但這些 getter 提供充足的資訊完成顯示的任務,但如果要修改暱稱,由於沒有setter,是無法擅自修改暱稱,只能透過 UserManager 進行修改。

Figure 1 UserInfo, User, and UserManager

Figure 1 UserInfo, User, and UserManager

這只是 program to interface 一個小把戲,事實上,傳遞給 UI 和UserManager 所持有的物件可以是同一個物件,只是彼此看到的介面不同罷了。例如下面範例程式中的 DefaultUserManager 實際上所持有的是 User 實體。像這樣的技巧在 iOS 開發時也很常用,內部持有的是 NSMutableArray 但傳出去時,卻是以 NSArray 往外傳。

當 UI 需要取得 UserInfo 時,呼叫 UserManager 的 getUser(String),實作中先從記憶體當中先找有沒有符合的實體,若沒有則呼叫 Web service 嘗試向 server 取得指定的使用者資訊,若沒有發生任何錯誤,Web service 一樣會回傳 immutable 的 UserInfo 實體,接著呼叫 DAO 將該使用者的資訊儲存到資料庫中,若沒有錯誤,用該資訊建立一個 User 實體放入users 中,下次使用時不需再呼叫 Web service,接著回傳 User 實體,但對 UI 而言取得的是 immutable object。若擔心強制轉型,可複製一份再回傳。

其他妙用

由於資料物件常常有多個屬性要初始化,以 User 為例,有三個屬性要初始化,若都透過 constructor 建立,constructor 的參數會變得相當多,這變成 long parameter list 的壞味道,此時,UserInfo 正好扮演 Parameter Object 的規範,而且還是 immutable 的 parameter object (雖然 Java 有 final 關鍵字可以用,但只限制該變數無法改變所指向的物件,無法阻止呼叫 setter 改變物件內的狀態)。這樣也可以避免常常寫「用預設建構子建立物件,然後呼叫很多的 setter 完成初始化」這類重複的程式碼。

以剛剛的 getUser 為例,如果是一個回傳 JSON 的 Web service,只需替 JSON parser 分析出來的結果寫一個 wrapper (如下),就能當做是建立 User 實體所需要的 parameter object 了,雖然很多 JSON parser 都有 data binding 的功能,能直接將 JSON 轉成對應的 POJO 物件,可是如果 JSON 內的屬性名稱和 POJO 的屬性名稱不相同時,在 model object 中加入與 domain無關的 annotation (例如 @SerializedName)就讓我有點討厭。

此外,Java 不像 C++ 支援 copy constructor,雖然提供一個 clone() 函式用來複製既有的物件,但使用上,須在想支援複製的類別上加上 Cloneable 的實作,否則呼叫 clone() 時會拋出CloneNotSupportedException 例外,而且 clone() 的回傳值是 Object 型別,每次都要轉型確實有點討厭。但有類似 Code List 3 的建構子後 (其實有點像copy constructor),複製一個物件,只需將要被複製當成建構子的參數即可。使用的方式就像 Code List 5 一樣,由於 DAO 的 save(UserInfo) 函式需要一個帶有最新狀態的物件好將狀態寫入資料庫,但在沒有成功之前,又不想改變既有的物件狀態,此時只需複製一份新的物件,將新的暱稱設定到複製出來的物件內,當成save(UserInfo) 的參數,若儲存成功,在將新的暱稱設回原先的物件,如此一來,若儲存失敗,既有的物件也不會受影響。

複雜的結構

Java 的 interface 可以繼承另一個 interface,所以 Immutable interface 也可以用在原有的繼承架構上,只是會變成像 Figure 2 那樣是個平行的繼承架構。最近 IM (Instant Messaging) 很熱門,有各式各樣的 IM App,而且傳遞的訊息也不再僅僅是文字訊息,以程式來說,要表現出不同的訊息類型,可能會有 Figure 2 右半邊的繼承架構,一個抽象類別 Message 代表一則訊息,實際的訊息種類則用 TextMessageImageMessage 分別代表文字訊息或是圖片訊息。在導入 Immutable interface 時,為了符合右邊的繼承架構,可以看到 Figure 2 左半邊有一個相似的繼承關係,在這繼承關係中都有一個對應的 Immutable interface,MessageInfo 對應 MessageTextMessageInfo 對應TextMessage,而 ImageMessageInfo 對應ImageMessage

Figure 2 immutable interface 繼承架構

Figure 2 immutable interface 繼承架構

在這樣的平行繼承關係中,先前提到的建構子一樣能適用,如 Code List 6 中,TextMessage 的建構子直接把參數傳給 Message 的建構子,實際上,一則文字訊息一旦送出,內容不會在修改 (好吧,最近有些 IM 主打閱過即焚,就好像 Mission: Impossible 中的任務錄影帶一樣),所以 TextMessage 除了透過建構子將訊息內容傳入外,並沒有 setter 能夠修改訊息內容。

總結

從上面的例子,可以看出 Immutable interface 讓封裝更有彈性,不用擔心 setter 的過度開放。當不希望物件被不允許的對象修改時,只需讓對方取得 getter 的介面即可,反之,讓能夠允許修改的對象取得有 setter 的物件即可。雖然沒有提到 functional programming,但 immutable object 是 functional programming 中很重要的一環,也是常用的一種 thread-safe 技巧,Immutable interface 某種程度上 (若不理會強制轉型),不需複製物件就能達成 immutable object 的目標了。


後記

雖然 C++ 已經有相當久的歷史,但若善用 const 關鍵字(使用 const point 或 const reference 搭配 const member functions),確實不需要 Immutable interface 就能輕鬆達成 immutable object 的目的。

進階參考


avatar-img
53會員
104內容數
這是從 Medium 開始的一個專題,主要是想用輕鬆閒談的方式,分享這幾年軟體開發的心得,原本比較侷限於軟體架構,但這幾年的文章不僅限於架構,也聊不少流程相關的心得,所以趁換平台,順勢換成閒談軟體設計。
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
Spirit的沙龍 的其他內容
RFC 4122 文件中,版本 3 和版本 5 比較特殊,是以名字產生 UUID,在特定條件下,每次產生的 UUID 都應該是一樣的。當時的情境用不到 v3 或 v5,但現在的情境就非常合適用 v3 或 v5,因為我們希望的是對同一筆資料,不管是 email 或是交易紀錄,都能產生同樣的 UUID。
UUID 有個致命的缺點是,它實在太長了,128 bits 用 Hex 表示法,至少要 32 個字元,如果再加上分隔符號,就要 36 個字元,把這放在面向使用者者的頁面上,應該不會有人會記得住吧!但 UUID 就真的只能這麼長嗎?其實是可以再短一點的。
這是 UUID 三部曲的首部曲,接下來兩周陸續更新第二部與第三部,首部曲先聊聊 UUID 的前世今生,二部曲會聊聊如何縮短 UUID 的長度,第三部曲則是聊聊特殊應用,希望能幫助到大家。
在大型主機的年代 (這裡要澄清一下,本人沒有歷經那個年代,不然都把自己講老了),就已經有 clent server 的概念:由 client (計算能力較差的終端機) 向 server (計算能力較好的大型主機) 請求服務,幫助理解與管理分散式系統程式的複雜度。
每次看到 REST 就讓我想起以前念研究所和老師一起想論文題目時,曾提過國外常會玩這種文字遊戲,像是將 Representational State Transfer 變成一個很簡單的單字 REST,但我的東西不管怎麼想,卻想不出什麼有趣的東西 Orz
稍微複雜一點的 query 其實代表著某些商業邏輯,若把這一段程式放到 repository 的實作層,會變成這些商業邏輯被隱藏起來了,如果有個好的描述語言,我倒覺得很好讀,也可以清楚知道背後的商業邏輯是什麼,是很好的一件事。
RFC 4122 文件中,版本 3 和版本 5 比較特殊,是以名字產生 UUID,在特定條件下,每次產生的 UUID 都應該是一樣的。當時的情境用不到 v3 或 v5,但現在的情境就非常合適用 v3 或 v5,因為我們希望的是對同一筆資料,不管是 email 或是交易紀錄,都能產生同樣的 UUID。
UUID 有個致命的缺點是,它實在太長了,128 bits 用 Hex 表示法,至少要 32 個字元,如果再加上分隔符號,就要 36 個字元,把這放在面向使用者者的頁面上,應該不會有人會記得住吧!但 UUID 就真的只能這麼長嗎?其實是可以再短一點的。
這是 UUID 三部曲的首部曲,接下來兩周陸續更新第二部與第三部,首部曲先聊聊 UUID 的前世今生,二部曲會聊聊如何縮短 UUID 的長度,第三部曲則是聊聊特殊應用,希望能幫助到大家。
在大型主機的年代 (這裡要澄清一下,本人沒有歷經那個年代,不然都把自己講老了),就已經有 clent server 的概念:由 client (計算能力較差的終端機) 向 server (計算能力較好的大型主機) 請求服務,幫助理解與管理分散式系統程式的複雜度。
每次看到 REST 就讓我想起以前念研究所和老師一起想論文題目時,曾提過國外常會玩這種文字遊戲,像是將 Representational State Transfer 變成一個很簡單的單字 REST,但我的東西不管怎麼想,卻想不出什麼有趣的東西 Orz
稍微複雜一點的 query 其實代表著某些商業邏輯,若把這一段程式放到 repository 的實作層,會變成這些商業邏輯被隱藏起來了,如果有個好的描述語言,我倒覺得很好讀,也可以清楚知道背後的商業邏輯是什麼,是很好的一件事。
你可能也想看
Google News 追蹤
Thumbnail
嘿,大家新年快樂~ 新年大家都在做什麼呢? 跨年夜的我趕工製作某個外包設計案,在工作告一段落時趕上倒數。 然後和兩個小孩過了一個忙亂的元旦。在深夜時刻,看到朋友傳來的解籤網站,興致勃勃熬夜體驗了一下,覺得非常好玩,或許有人玩過了,但還是想寫上來分享紀錄一下~
## 存取修飾詞詳解 在 C# 中有四種主要的存取修飾詞,但作為初學者,你最常用到的是以下兩種: 1. **Public**: 任何腳本都可以無限制地訪問 2. **Private**: 只能在它們被創建的類別(稱為包含類別)中訪問。任何沒有存取修飾詞的變數預設都是 Private
Thumbnail
本章節是Java入門的第八天,主要介紹物件導向的概念。這包括了類別、建構子、存取修飾子、繼承、多型、封裝、介面、抽象類別、靜態類別、列舉、Lambda表達式、泛型和反射等主題。每個主題都配有相關的程式碼範例,以協助讀者更好地理解這些概念。
Thumbnail
這個章節主要介紹了Swift程式語言中物件導向程式設計的基本概念,包括類別、建構子、公開、私有、受保護等等的概念。同時,也介紹了繼承、多型、封裝、介面、抽象類別、靜態類別、列舉、委派、Lambda表達式、泛型和反射等進階特性。
setter和getter能把狀態改變時需做的事情包裝起來,讓外部只需簡單修改參數就能達到預想的效果
Thumbnail
※ OPP第一大核心-封裝 封裝的精神在於將「方法」、「屬性」和「邏輯」包裝在類別裡面,透過類別的實例來實現。這樣外部物件不需要了解內部的實現細節,只需要知道如何使用該類別提供的接口即可。換句話說,封裝是將內部細節隱藏起來,只暴露必要的部分給使用者。 封裝的核心概念是,使用者如果想要接觸資料,只
※ Object(物件) & Constructor Function(建構式函式) Object(物件)是什麼? 物件是一種「可以將資料、程式碼包含在其中」的資料結構。 Object(物件)的兩種創造方式: 匿名物件:直接使用"{}"。沒有特別的名字,直接從Object中繼承過來的一個物件
函數式編程跟物件導向一個很大的差異在於對資料可變性(mutability)的態度,函數式編程不鼓勵修改原有的資料,有些語言甚至沒有修改的概念;而物件導向專注於狀態的改變,物件作為閉包就已經假設資料是可變的。這種對於可變性的態度注定物件導向比較容易得到關注,因為這個模型比較符合電腦底層的運作邏輯,而我
物件導向設計的一個重點就是封裝,這有很多層面上的意義,但基本上就是控制物件的成員變數和方法的存取權。物件導向的封裝還跟繼承機制有關,這使得有一些時候我們逼不得已必須把函式定義在類別上,這種做法使得物件的功能變得難以拆解。封裝應該是模組的職責,並不需要再給物件相同的能力。 一般的模組系統就是把相
類似於trait/typeclass的特性系統能提供程式「延展性」,它能讓函式針對不同的類型做出不同的行為。這種機制與物件導向的繼承非常像,然而特性系統的彈性比較大一點,而且概念上也有一些差別。為了探討討論這些差異,我們必須深入了解繼承機制到底是什麼。 繼承並不是建立子類關係的唯一方法。所謂的
Thumbnail
嘿,大家新年快樂~ 新年大家都在做什麼呢? 跨年夜的我趕工製作某個外包設計案,在工作告一段落時趕上倒數。 然後和兩個小孩過了一個忙亂的元旦。在深夜時刻,看到朋友傳來的解籤網站,興致勃勃熬夜體驗了一下,覺得非常好玩,或許有人玩過了,但還是想寫上來分享紀錄一下~
## 存取修飾詞詳解 在 C# 中有四種主要的存取修飾詞,但作為初學者,你最常用到的是以下兩種: 1. **Public**: 任何腳本都可以無限制地訪問 2. **Private**: 只能在它們被創建的類別(稱為包含類別)中訪問。任何沒有存取修飾詞的變數預設都是 Private
Thumbnail
本章節是Java入門的第八天,主要介紹物件導向的概念。這包括了類別、建構子、存取修飾子、繼承、多型、封裝、介面、抽象類別、靜態類別、列舉、Lambda表達式、泛型和反射等主題。每個主題都配有相關的程式碼範例,以協助讀者更好地理解這些概念。
Thumbnail
這個章節主要介紹了Swift程式語言中物件導向程式設計的基本概念,包括類別、建構子、公開、私有、受保護等等的概念。同時,也介紹了繼承、多型、封裝、介面、抽象類別、靜態類別、列舉、委派、Lambda表達式、泛型和反射等進階特性。
setter和getter能把狀態改變時需做的事情包裝起來,讓外部只需簡單修改參數就能達到預想的效果
Thumbnail
※ OPP第一大核心-封裝 封裝的精神在於將「方法」、「屬性」和「邏輯」包裝在類別裡面,透過類別的實例來實現。這樣外部物件不需要了解內部的實現細節,只需要知道如何使用該類別提供的接口即可。換句話說,封裝是將內部細節隱藏起來,只暴露必要的部分給使用者。 封裝的核心概念是,使用者如果想要接觸資料,只
※ Object(物件) & Constructor Function(建構式函式) Object(物件)是什麼? 物件是一種「可以將資料、程式碼包含在其中」的資料結構。 Object(物件)的兩種創造方式: 匿名物件:直接使用"{}"。沒有特別的名字,直接從Object中繼承過來的一個物件
函數式編程跟物件導向一個很大的差異在於對資料可變性(mutability)的態度,函數式編程不鼓勵修改原有的資料,有些語言甚至沒有修改的概念;而物件導向專注於狀態的改變,物件作為閉包就已經假設資料是可變的。這種對於可變性的態度注定物件導向比較容易得到關注,因為這個模型比較符合電腦底層的運作邏輯,而我
物件導向設計的一個重點就是封裝,這有很多層面上的意義,但基本上就是控制物件的成員變數和方法的存取權。物件導向的封裝還跟繼承機制有關,這使得有一些時候我們逼不得已必須把函式定義在類別上,這種做法使得物件的功能變得難以拆解。封裝應該是模組的職責,並不需要再給物件相同的能力。 一般的模組系統就是把相
類似於trait/typeclass的特性系統能提供程式「延展性」,它能讓函式針對不同的類型做出不同的行為。這種機制與物件導向的繼承非常像,然而特性系統的彈性比較大一點,而且概念上也有一些差別。為了探討討論這些差異,我們必須深入了解繼承機制到底是什麼。 繼承並不是建立子類關係的唯一方法。所謂的