閒談軟體設計: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
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
11/20日NVDA即將公布最新一期的財報, 今天Sell Side的分析師, 開始調高目標價, 市場的股價也開始反應, 未來一週NVDA將重新回到美股市場的焦點, 今天我們要分析NVDA Sell Side怎麼看待這次NVDA的財報預測, 以及實際上Buy Side的倉位及操作, 從
Thumbnail
Hi 大家好,我是Ethan😊 相近大家都知道保濕是皮膚保養中最基本,也是最重要的一步。無論是在畫室裡長時間對著畫布,還是在旅途中面對各種氣候變化,保持皮膚的水分平衡對我來說至關重要。保濕化妝水不僅能迅速為皮膚補水,還能提升後續保養品的吸收效率。 曾經,我的保養程序簡單到只包括清潔和隨意上乳液
Thumbnail
最近身旁有幾位正在懷孕、或剛生產完的朋友,讓我想起自己在懷孕期間印象最深刻的三件「怪事」,其中又以第三件事最誇張。
Thumbnail
不知道大家在買房之前是不是都會參考親朋好友的意見,或是上網看一些買房注意事項,有時候考慮了這塊就忘了那塊,考慮的那塊又忘了這塊.......
Thumbnail
塔西佗陷阱(Tacitus Trap),一個得名於古羅馬歷史學家塔西佗的政治學理論,意指倘若公權力失去其公信力,無論如何發言或是處事,社會均將給予其負面評價。 當然信著恆信、不信者恆不信,這就是真實的人生。
Thumbnail
關於片名   台灣片名《花漾女子》,原文片名《Promising Young Woman》,台灣譯名將時間定格在悲劇發生前,而原文片名則進一步帶我們看見另一個可能性結果
Thumbnail
前幾年因為身體的關係,當了幾年的律師逃兵,當時開了之前的事務所以後,一時間也沒有特別想要做甚麼事情,所以就邊讀一點書、早晚運動一下,剛好聽到當年同梯朋友進去金融業工作,因此也抱著嘗(ㄊㄠˊ)試(ㄅㄧˋ)的心態,找了份銀行法令遵循的工作
Thumbnail
那天我問隊友,怎樣才算是一部小說呢?按字數計算嗎? 他說:「故事內起承轉合都有,就算」 所以我大膽地按著他的標準,將自己寫過的故事,粗略整理出一個明細。
纏中說禪,本名李彪,專欄筆名木子,其人是中國股市比較早期的操盤手,所以他比較熟悉a股的市場情況。他以“纏中說禪”為筆名,從2002年開始寫博客,直到2008年癌症病重停更,期間寫下了不少文章。而博客文章中最為著名的就是他的“教你炒股票”系列文章,他在這個系列裡講到的炒股理論和方法被粉絲稱為“纏論
Thumbnail
婚姻是人生大事,對溥儀尤其如此,因為如果皇帝大婚,就代表溥儀可以脫離眾多便宜老媽的束縛而得以親政。 但詭異的是,這個可以讓他脫離便宜老媽掌控的婚姻,卻還是要由便宜老媽進行主導並且居中角力......
Thumbnail
上次我提到:溥儀就是個死小孩。其實這不能全怪溥儀,而要怪詭異的宮廷教育及生活制度......
Thumbnail
近期電影「末代皇帝」重新修復上映。 為了推坑這部經典之作,本人決定以溥儀本身的自傳《我的前半生》為主要基底,和大家談一些電影中礙於篇幅或是藝術改編,而不容易察覺或是沒有呈現的真實歷史。
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
11/20日NVDA即將公布最新一期的財報, 今天Sell Side的分析師, 開始調高目標價, 市場的股價也開始反應, 未來一週NVDA將重新回到美股市場的焦點, 今天我們要分析NVDA Sell Side怎麼看待這次NVDA的財報預測, 以及實際上Buy Side的倉位及操作, 從
Thumbnail
Hi 大家好,我是Ethan😊 相近大家都知道保濕是皮膚保養中最基本,也是最重要的一步。無論是在畫室裡長時間對著畫布,還是在旅途中面對各種氣候變化,保持皮膚的水分平衡對我來說至關重要。保濕化妝水不僅能迅速為皮膚補水,還能提升後續保養品的吸收效率。 曾經,我的保養程序簡單到只包括清潔和隨意上乳液
Thumbnail
最近身旁有幾位正在懷孕、或剛生產完的朋友,讓我想起自己在懷孕期間印象最深刻的三件「怪事」,其中又以第三件事最誇張。
Thumbnail
不知道大家在買房之前是不是都會參考親朋好友的意見,或是上網看一些買房注意事項,有時候考慮了這塊就忘了那塊,考慮的那塊又忘了這塊.......
Thumbnail
塔西佗陷阱(Tacitus Trap),一個得名於古羅馬歷史學家塔西佗的政治學理論,意指倘若公權力失去其公信力,無論如何發言或是處事,社會均將給予其負面評價。 當然信著恆信、不信者恆不信,這就是真實的人生。
Thumbnail
關於片名   台灣片名《花漾女子》,原文片名《Promising Young Woman》,台灣譯名將時間定格在悲劇發生前,而原文片名則進一步帶我們看見另一個可能性結果
Thumbnail
前幾年因為身體的關係,當了幾年的律師逃兵,當時開了之前的事務所以後,一時間也沒有特別想要做甚麼事情,所以就邊讀一點書、早晚運動一下,剛好聽到當年同梯朋友進去金融業工作,因此也抱著嘗(ㄊㄠˊ)試(ㄅㄧˋ)的心態,找了份銀行法令遵循的工作
Thumbnail
那天我問隊友,怎樣才算是一部小說呢?按字數計算嗎? 他說:「故事內起承轉合都有,就算」 所以我大膽地按著他的標準,將自己寫過的故事,粗略整理出一個明細。
纏中說禪,本名李彪,專欄筆名木子,其人是中國股市比較早期的操盤手,所以他比較熟悉a股的市場情況。他以“纏中說禪”為筆名,從2002年開始寫博客,直到2008年癌症病重停更,期間寫下了不少文章。而博客文章中最為著名的就是他的“教你炒股票”系列文章,他在這個系列裡講到的炒股理論和方法被粉絲稱為“纏論
Thumbnail
婚姻是人生大事,對溥儀尤其如此,因為如果皇帝大婚,就代表溥儀可以脫離眾多便宜老媽的束縛而得以親政。 但詭異的是,這個可以讓他脫離便宜老媽掌控的婚姻,卻還是要由便宜老媽進行主導並且居中角力......
Thumbnail
上次我提到:溥儀就是個死小孩。其實這不能全怪溥儀,而要怪詭異的宮廷教育及生活制度......
Thumbnail
近期電影「末代皇帝」重新修復上映。 為了推坑這部經典之作,本人決定以溥儀本身的自傳《我的前半生》為主要基底,和大家談一些電影中礙於篇幅或是藝術改編,而不容易察覺或是沒有呈現的真實歷史。