閒談軟體設計: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
留言分享你的想法!
Spirit-avatar-img
發文者
2024/05/23
書摘《設計重構》提及了這篇文章,趕快過去看看吧!
avatar-img
Spirit的沙龍
54會員
106內容數
這是從 Medium 開始的一個專題,主要是想用輕鬆閒談的方式,分享這幾年軟體開發的心得,原本比較侷限於軟體架構,但這幾年的文章不僅限於架構,也聊不少流程相關的心得,所以趁換平台,順勢換成閒談軟體設計。
Spirit的沙龍的其他內容
2024/03/23
這篇文章探討了在軟體開發中的技術債可能來自哪些原因,以及如何自動化偵測與修復技術債。作者透過分享不同情境下的技術債選擇,提供了對於技術債的思考與建議,針對開發人員在需要做出無奈的技術決策時,提供了一些建議。此外,還提供了一些在做出技術決策時的方法,如保留抽象層和避免vendor lock-in。
Thumbnail
2024/03/23
這篇文章探討了在軟體開發中的技術債可能來自哪些原因,以及如何自動化偵測與修復技術債。作者透過分享不同情境下的技術債選擇,提供了對於技術債的思考與建議,針對開發人員在需要做出無奈的技術決策時,提供了一些建議。此外,還提供了一些在做出技術決策時的方法,如保留抽象層和避免vendor lock-in。
Thumbnail
2024/03/09
今天來聊個最近很夯的主題 DDD,但不是 DDD 的本尊 Domain Driven Design,而是無所不在的 Database Driven Design,Database Driven Design 不是不好,只是你的模型容易變成貧血模型,邏輯都集中在 service 層等等。
Thumbnail
2024/03/09
今天來聊個最近很夯的主題 DDD,但不是 DDD 的本尊 Domain Driven Design,而是無所不在的 Database Driven Design,Database Driven Design 不是不好,只是你的模型容易變成貧血模型,邏輯都集中在 service 層等等。
Thumbnail
2024/03/02
有趣的是,Model 其實沒什麼嚴格的定義,所以每個人對 Model 的解讀也不盡相同,有人覺得資料怎麼儲存屬於 Model 的一部份 (受 ORM 工具的影響),有人覺得工作流程 (workflow) 是 Model 的一部份,我個人也有自己的想法,而且隨專案的規模和特性,也不是總是一樣的。
Thumbnail
2024/03/02
有趣的是,Model 其實沒什麼嚴格的定義,所以每個人對 Model 的解讀也不盡相同,有人覺得資料怎麼儲存屬於 Model 的一部份 (受 ORM 工具的影響),有人覺得工作流程 (workflow) 是 Model 的一部份,我個人也有自己的想法,而且隨專案的規模和特性,也不是總是一樣的。
Thumbnail
看更多
你可能也想看
Thumbnail
每年4月、5月都是最多稅要繳的月份,當然大部份的人都是有機會繳到「綜合所得稅」,只是相當相當多人還不知道,原來繳給政府的稅!可以透過一些有活動的銀行信用卡或電子支付來繳,從繳費中賺一點點小確幸!就是賺個1%~2%大家也是很開心的,因為你們把沒回饋變成有回饋,就是用卡的最高境界 所得稅線上申報
Thumbnail
每年4月、5月都是最多稅要繳的月份,當然大部份的人都是有機會繳到「綜合所得稅」,只是相當相當多人還不知道,原來繳給政府的稅!可以透過一些有活動的銀行信用卡或電子支付來繳,從繳費中賺一點點小確幸!就是賺個1%~2%大家也是很開心的,因為你們把沒回饋變成有回饋,就是用卡的最高境界 所得稅線上申報
Thumbnail
全球科技產業的焦點,AKA 全村的希望 NVIDIA,於五月底正式發布了他們在今年 2025 第一季的財報 (輝達內部財務年度為 2026 Q1,實際日曆期間為今年二到四月),交出了打敗了市場預期的成績單。然而,在銷售持續高速成長的同時,川普政府加大對於中國的晶片管制......
Thumbnail
全球科技產業的焦點,AKA 全村的希望 NVIDIA,於五月底正式發布了他們在今年 2025 第一季的財報 (輝達內部財務年度為 2026 Q1,實際日曆期間為今年二到四月),交出了打敗了市場預期的成績單。然而,在銷售持續高速成長的同時,川普政府加大對於中國的晶片管制......
Thumbnail
重點摘要: 6 月繼續維持基準利率不變,強調維持高利率主因為關稅 點陣圖表現略為鷹派,收斂 2026、2027 年降息預期 SEP 連續 2 季下修 GDP、上修通膨預測值 --- 1.繼續維持利率不變,強調需要維持高利率是因為關稅: 聯準會 (Fed) 召開 6 月利率會議
Thumbnail
重點摘要: 6 月繼續維持基準利率不變,強調維持高利率主因為關稅 點陣圖表現略為鷹派,收斂 2026、2027 年降息預期 SEP 連續 2 季下修 GDP、上修通膨預測值 --- 1.繼續維持利率不變,強調需要維持高利率是因為關稅: 聯準會 (Fed) 召開 6 月利率會議
Thumbnail
※ OPP第一大核心-封裝 封裝的精神在於將「方法」、「屬性」和「邏輯」包裝在類別裡面,透過類別的實例來實現。這樣外部物件不需要了解內部的實現細節,只需要知道如何使用該類別提供的接口即可。換句話說,封裝是將內部細節隱藏起來,只暴露必要的部分給使用者。 封裝的核心概念是,使用者如果想要接觸資料,只
Thumbnail
※ OPP第一大核心-封裝 封裝的精神在於將「方法」、「屬性」和「邏輯」包裝在類別裡面,透過類別的實例來實現。這樣外部物件不需要了解內部的實現細節,只需要知道如何使用該類別提供的接口即可。換句話說,封裝是將內部細節隱藏起來,只暴露必要的部分給使用者。 封裝的核心概念是,使用者如果想要接觸資料,只
Thumbnail
public: 可以在任何地方存取(access) private: 只能在同class中存取 default: 只能在同package中存取 protected: 只能在同package,以及它的子class存取。不能在不同package的非子class存取
Thumbnail
public: 可以在任何地方存取(access) private: 只能在同class中存取 default: 只能在同package中存取 protected: 只能在同package,以及它的子class存取。不能在不同package的非子class存取
Thumbnail
不管用哪種語言開發軟體,除非是那種一個 function 寫個幾萬行的人 (來人啊,把這種人拖去砍了),不然,一般都會根據某些因素,切割成模組或是特定功能的區塊 (一個 class 或是一個 function),但要完成一個特定功能,這些模組或區塊勢必要一起合作,因此這些模組與區塊就發生了關係。
Thumbnail
不管用哪種語言開發軟體,除非是那種一個 function 寫個幾萬行的人 (來人啊,把這種人拖去砍了),不然,一般都會根據某些因素,切割成模組或是特定功能的區塊 (一個 class 或是一個 function),但要完成一個特定功能,這些模組或區塊勢必要一起合作,因此這些模組與區塊就發生了關係。
Thumbnail
這篇文章將會講述 Unity C# 中關於 Interface (介面/接口)的基本介紹以及原理說明,最後提供完整的使用流程。
Thumbnail
這篇文章將會講述 Unity C# 中關於 Interface (介面/接口)的基本介紹以及原理說明,最後提供完整的使用流程。
Thumbnail
,先來分享一下封裝是怎麼一回事。 一、封裝(Encapsulation) 封裝就是把一些功能的處理程序或是資料包起來,也對於程式碼做權限的設定做一層保護的機制,這是為了防止程式碼被竄改,所以有了封裝可以保障我們資料的隱密性,甚至封裝也是一種將一些處理程序隱藏起來,讓使用者使用時可以更加單純。 1.什
Thumbnail
,先來分享一下封裝是怎麼一回事。 一、封裝(Encapsulation) 封裝就是把一些功能的處理程序或是資料包起來,也對於程式碼做權限的設定做一層保護的機制,這是為了防止程式碼被竄改,所以有了封裝可以保障我們資料的隱密性,甚至封裝也是一種將一些處理程序隱藏起來,讓使用者使用時可以更加單純。 1.什
Thumbnail
一、存取修飾詞public / private / protected / internal 二、參數修飾詞ref / in / out >>>>>由於我們在寫程式時,會去宣告一些變數、常數相關識別詞,並且在class(類別)中會寫一些事情或動作讓程式去運行,然而這個概念就是去定義對於我們所寫的內容
Thumbnail
一、存取修飾詞public / private / protected / internal 二、參數修飾詞ref / in / out >>>>>由於我們在寫程式時,會去宣告一些變數、常數相關識別詞,並且在class(類別)中會寫一些事情或動作讓程式去運行,然而這個概念就是去定義對於我們所寫的內容
Thumbnail
承接上一段,接下來到了一段Rust比較新奇的部分也是控制記憶體的部分AKA所有權。 Rust 程式設計語言 所有權是在Rust處理記憶體的機制,記憶體由所有權系統管理,且編譯器會在編譯時加上一些規則檢查。 在這之前需要知道的部分 每個變數有一個所有者(owner) 同時間只能有一個所有者 只要擁有者
Thumbnail
承接上一段,接下來到了一段Rust比較新奇的部分也是控制記憶體的部分AKA所有權。 Rust 程式設計語言 所有權是在Rust處理記憶體的機制,記憶體由所有權系統管理,且編譯器會在編譯時加上一些規則檢查。 在這之前需要知道的部分 每個變數有一個所有者(owner) 同時間只能有一個所有者 只要擁有者
Thumbnail
這篇文章主要介紹存取子讀(get)和寫(set)的概述,並且簡單介紹基本的使用方法以及其他程式呼叫範例。
Thumbnail
這篇文章主要介紹存取子讀(get)和寫(set)的概述,並且簡單介紹基本的使用方法以及其他程式呼叫範例。
Thumbnail
使用者自訂的資料型別
Thumbnail
使用者自訂的資料型別
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News