閒談軟體設計:Repository

閱讀時間約 13 分鐘

前言

我想用過 Spring Data JPA 的開發者對 repository 應該都蠻熟悉,只要宣告個 介面,加上完備的 JPA annotation,Spring Data JPA 會自動 inject 實作,十分方便。我則是在 Martin Fowler 的《Patterns of Enterprise Application Architecture》(後面以 PoEEA 代表) 書中看到 Repository,當初覺得跟 DAO 很像啊,沒花太多心思在比較兩者的差異,後來在建構旅遊 app 內容管理平台時,開始思考該怎麼設計才好?回頭翻書找 Repository 的描述
Mediates between the domain and data mapping layers using a collection-like interface for accessing domain objects.
簡單說,Repository 提供像是 array 或是 dictionary 的容器,對程式來說,就好像記憶體中有所有的物件,不用去想物件其實是從資料庫來的。作法是在 data mapping layer 之上再提供一個 collection 的介面來存取物件,將資料庫的細節從商業邏輯層抽離。data mapping 很複雜,PoEAA 書中有個獨立的 pattern:Data Mapper 說明這件事,容我找個時間另外寫一篇,這邊先簡單用例子說明,假設是 RDBMS,Data Mapper 便是處理 SQL statement 的生成,以及查詢後將結果轉換成 domain object 的一個專屬物件。

Collection-like interface

為什麼是 collection-like 呢?因為所有語言 (即便不是物件導向語言) 的基礎函式庫都有 array 及 dictionary 容器,將資料寫入資料庫類比成將資料放進容器,從資料庫讀取資料類比成從容器拿取物件,將資料從資料庫刪除則類比成從容器中移除物件,對沒接觸過資料庫的開發者,也能用熟悉的 array 及 dictionary 來操作。
就拿 Java 來說,Java 有一個非常完整 (針對使用情境有不同的最佳化實作) 的 Java Collections Framework,而 Spring Data 的 CrudRepository 介面幾乎和 Java 的 Map 近似,名稱雖不同,但意思幾乎是一樣的。
Figure 1 - Comparison between Java Collection interfaces and CrudRepository
至於 Repository 的介面該不該有 save 或 update (saveOrUpdate) 函式,老實說,過去我都用過,但在看過 PoEAA 書中的說明後,若要新寫一個,我可能會宣告 GenericRepository 像下面這樣:
get 用 ID 從資料庫取得特定物件,put 則是將物件存入資料庫,至於這物件是要 save 或是 update 則是由 repository 的實作決定 (PoEAA 有提到,可以用 Identity Map 判斷資料庫是否已經有該物件),remove 則是從資料庫移除該物件,contains 則是判斷資料庫中是否有這個物件,後面兩個要不要提供能給予 entity 及 ID 的 overloading methods 我是覺得都可以,大多數 Map 或 Dictionary 的介面都有類似的作法,clear 是清除整個資料庫 (表),最後 count 是詢問資料庫有多少這個類型的物件。
有幾個 methods 是可選的,考量到 performance,keys 和 values 大多數情況下不會呼叫,畢竟把所有資料載入到記憶體當中不太實際,反而 putAll 和 removeAll 則是為了批次寫入,而特定加進去的,而且在多數 collection 介面中也會看到類似的 method,並沒有破壞 collection-like 的原則。
反而最容易破壞 collection-like 介面的是例外 (exception),辛辛苦苦把資料庫的細節封裝在 repository 內部,但資料庫就是有可能無法連線、斷線或是發生 I/O 錯誤,這時候都會拋出例外 (好吧~有些語言可能不是以例外的形式),總不能把底層的例外直接拋出去,那不同資料庫就會有不同例外型別,原則上是將底層例外包裝再一次,例如 RepositoryException,至於在 Java 環境中這個例外該是 checked 還是 unchecked (介面會好看一點) 的,就不在這討論了,畢竟這問題已經有太多人 (激烈) 討論過了 (參閱 閒談軟體設計:例外處理 )。

Complex Query & Batch Update

剛剛有討論到,即使用了 Repository,抽象溢漏來是會發生,會拋出例外是一個例子,不適合呼叫 values 也是一個例子,一般來說,應用程式不太可能只有一筆一筆取資料的形式,很多情況會是撈取符合特定條件的物件,另外還會考慮記憶體使用量,會使用分頁,還好這種情境,可以抽象是從容器過濾 (filter) 出符合條件的物件,只是條件該用什麼形式表達,在 PoEAA 建議用 Query Object 以物件的方式描述,詳細的內容參考另一篇《閒談軟體設計:Query Object》吧!
複雜的查詢能用 Query Object 解決,但批次更新怎麼辦?我覺得應該還是可以用 Query Object 來處理,因為 SQL 的 query statement 是可以做批次處理的,擴充 query object 把批次處理放進去,但 repository 的介面要叫什麼?filter(batchUpdateQuery) 怪怪的,我曾想過 perform(BatchOperation),不太像 collection 原生的操作 ,但我覺得還不算太差。
說實話,我過去並沒有實作類似 solution 的經驗,但概念是從 CoreData 的 NSBatchUpdateRequest 借來的。BatchOperation 包含一個 Query Object 用來限定修改的範圍,然後包含一個描述修改的 Update Action (其實就是 Query Object,只是描述的都是修改),最後 repository 能將兩者結合並轉換成實際的 SQL statement 或是該資料庫對應的模型。
如此一來,剛剛的 removeAll(entities) 就可以在 interface 用 perform 提供預設的實作:
到目前為止,都是以一個物件或多個物件為單位進行操作,但實務上還是有可能只改某個屬性,這時候,就會變成從 repository 取出一個物件,然後修改屬性後再存回去 (嚴格來說,真正存在記憶體中的物件,是不用再呼叫一次 put,容器中的物件就應該能反映最新的狀態回資料庫,若要做到這種程度,repository 回傳的物件需是一個經過包裝過的 proxy,任何的 setter 的呼叫會將物件記為 dirty,當整個 transaction 要結束前,會自己將變動存入資料庫,這要自己做太複雜了,大多要靠 framework 幫忙),這做法沒什麼問題,但 data mapper 必須要能處理資料可能會 conflict 的情況。有趣的是,這情況反而是用修改單一欄位的 SQL statement 不會發生。
特別是在 server 端,多個 API requests 想改同一個物件的不同屬性時,就可能會出問題。例如 API request A 要改 entity.x,另一個 API request B 要改 entity.y,他們各自從 repository 取得了一個 entity 物件,修改完後,A 取得的 entity.y 仍是原先的值,而 B 取得的 entity.x 亦是 (一般來說,不同 thread 取得的都是不同物件實體),如果 repository 中的 data mapper 沒意識到這情況,若寫入順序是先 A 後 B,那 A 的修改就會被覆蓋掉,其他細節就留到介紹 Data Mapper 的文章中吧!

Transaction

既然提到 conflict,有人會想到 RDBMS 有提供交易 (transaction) 機制,確保資料的完整性和一制性,那 repository 要管理 transaction 的 life-cycle 嗎?例如:repository 每個需要寫入的 method 內先呼叫 beginTransaction,寫入後呼叫 commit,或是發生例外時呼叫 rollback 嗎?
我目前的想法是 transaction 應該跟著 business logic scope 走,repository 本身並不知道自己所處的 scope,資訊不足以管理 transaction life-cycle。假設有個 API 是檢查註冊帳號的驗證碼,如果驗證碼正確,會將帳號 (account) 的狀態 (state) 改為啟用 (active),然後產生一個認證 (authentication) 並回傳。
以這例子來說,會用到 AccountRepository 和 AuthenticationRepository,一個完整的 business logic scope 是兩個操作 (改狀態和產生認證) 都完成才算是完成,任何一個失敗,都不該保留個別的結果,也就是說 transaction 的管理在使用 repository 的人身上,當然,這可以推到其他層,把跟資料庫有關的抽象溢漏抽離到 business logic 之外,這時候,不太想依賴框架的我就很喜歡 Spring framework 提供的 @Transactional。
商業邏輯層的 AuthenticationManager 大概會像這樣,只有 10 行不到的程式應該很好懂吧,裡面沒有任何 transaction 管理的邏輯,讓 validateAuthCode 聚焦在驗證上。
但使用 AuthenticationManager 的人就要注意 transaction,以這個例子來說,使用者就是 AuthenticationWebService,這個類別其實做的事也很單純,就是負責 HTTP request,把請求的內容轉成 domain 需要的資訊,呼叫 manager 做事,然後再把 domain object 轉成 HTTP response 所需的內容。
如此 transaction 的 scope 和 AuthenticationWebService 的 validateAuthCode 是一致的,如果有任何 exception 從這個 method 拋出,Spring framework 的 transaction management request filter 會攔截這個 exception 然後 rollback,這樣一來 AuthenticationManager 確實就不用知道 transaction 的管理,當資料庫的種類跟來源不止一個時,Spring framework 提供的跨來源 transaction management 工具就更加方便了。

結語

其實 Repository 只是希望提供一個 collection-like 的抽象層,但要實作時,有很多菱菱角角的問題,每個人對於這些問題,在不同的 context 下,決策不完全一樣。例如, Repository 要不要支援 reactive programming or async?畢竟這兩個是最近很流行的技術,是增加 server 處理 requests 數量的方式之一,但這樣的設計對一個 collection 的抽象是否合適呢?我說不上來。又或者是,抽象層到底要做到多乾淨,為了這個乾淨,要付出多少工程上的代價,我只能說這都是每個軟體設計師每天進行的修練。
就以說明 Repository 的例子來說,程式範例可以再更精簡一點,例如不一定要用六角架構來設計 web service 和 manager,但後來想想,這樣的例子應該要更好讀才對,就維持當初構想的設計了,所以應該還好吧?

在 Medium 的版本中,有一個小節 Cache/Sharding with Decorator 並沒有被放進來,主要是因為最近想把這一節改寫成獨立的一篇文章,加上目前這文章也有五千多字了,就請等待之後獨立的文章吧!
即將進入廣告,捲動後可繼續閱讀
為什麼會看到廣告
52會員
102Content count
這是從 Medium 開始的一個專題,主要是想用輕鬆閒談的方式,分享這幾年軟體開發的心得,原本比較侷限於軟體架構,但這幾年的文章不僅限於架構,也聊不少流程相關的心得,所以趁換平台,順勢換成閒談軟體設計。
留言0
查看全部
發表第一個留言支持創作者!
Spirit的沙龍 的其他內容
不是在軟體建置之初就決定所有一切的行為,而是執行時根據配置 (configuration) 或執行時的環境 (例如:特定目錄有什麼 plug-in) 決定有哪些行為,話是這麼說,但要實作一個可以載入 plug-in 的軟體 (以下稱作 host application) 倒是有不少事情要考慮。
基本上就是這樣,例外處理是可以在軟體架構設計時就考慮進去,或者說,在軟體架構設計時就該考慮進去,制定方針讓團隊有一個原則可以遵循,透過設計讓例外的處理較容易與一致,最終讓軟體的品質可以更好。
設計時的考量主要有:(1) App 是 Internet App,在考量 UI 體驗和網路頻寬的消耗,多數資料需以某種形式儲存部分資料在行動裝置上;(2) 因此會需要同步伺服器端和行動裝置端之間資料狀態;(3) 但行動裝置網路的穩定性不如一般網路可靠,要有足夠的自動化測試驗證正常的流程與異常的流程。
程式開發有趣的地方,同樣的目標,不同的團隊會因不同的因素做出不同的設計抉擇。而這往往也是為什麼一個資深的工程師在開發速度上不一定比較快的原因之一,一個越是資深的工程師,思考的因素會更多,不過,不是考慮得越多就結果就一定越好,有時還會變成 over design 較糟的結果。
唸研究所開始當助教,偶而會有學弟妹問:怎樣寫好程式?老實說,這是個大哉問,連我學開發軟體這麼久,我也只能回答他們:多培養自己釐清問題、拆解問題、解決問題與抽象化的能力。但他們通常只會一臉狐疑看著我,感覺我說的話好抽象。
在履歷中常常看到導入 MVVM,然後問為什麼要導入 MVVM 時,最常聽到的答案是這樣不會有很肥大的 view controller,但如果再問 view controller 是 MVC 的那一個部分,很多人卻回答不出個所以然,所以想聊聊這個很多種說法的 MVC pattern。
不是在軟體建置之初就決定所有一切的行為,而是執行時根據配置 (configuration) 或執行時的環境 (例如:特定目錄有什麼 plug-in) 決定有哪些行為,話是這麼說,但要實作一個可以載入 plug-in 的軟體 (以下稱作 host application) 倒是有不少事情要考慮。
基本上就是這樣,例外處理是可以在軟體架構設計時就考慮進去,或者說,在軟體架構設計時就該考慮進去,制定方針讓團隊有一個原則可以遵循,透過設計讓例外的處理較容易與一致,最終讓軟體的品質可以更好。
設計時的考量主要有:(1) App 是 Internet App,在考量 UI 體驗和網路頻寬的消耗,多數資料需以某種形式儲存部分資料在行動裝置上;(2) 因此會需要同步伺服器端和行動裝置端之間資料狀態;(3) 但行動裝置網路的穩定性不如一般網路可靠,要有足夠的自動化測試驗證正常的流程與異常的流程。
程式開發有趣的地方,同樣的目標,不同的團隊會因不同的因素做出不同的設計抉擇。而這往往也是為什麼一個資深的工程師在開發速度上不一定比較快的原因之一,一個越是資深的工程師,思考的因素會更多,不過,不是考慮得越多就結果就一定越好,有時還會變成 over design 較糟的結果。
唸研究所開始當助教,偶而會有學弟妹問:怎樣寫好程式?老實說,這是個大哉問,連我學開發軟體這麼久,我也只能回答他們:多培養自己釐清問題、拆解問題、解決問題與抽象化的能力。但他們通常只會一臉狐疑看著我,感覺我說的話好抽象。
在履歷中常常看到導入 MVVM,然後問為什麼要導入 MVVM 時,最常聽到的答案是這樣不會有很肥大的 view controller,但如果再問 view controller 是 MVC 的那一個部分,很多人卻回答不出個所以然,所以想聊聊這個很多種說法的 MVC pattern。
你可能也想看
Thumbnail
1.加權指數與櫃買指數 週五的加權指數在非農就業數據開出來後,雖稍微低於預期,但指數仍向上噴出,在美股開盤後於21500形成一個爆量假突破後急轉直下,就一路收至最低。 台股方面走勢需觀察週一在斷頭潮出現後,週二或週三開始有無買單進場支撐,在沒有明確的反轉訊號形成前,小夥伴盡量不要貿然抄底,或是追空
Thumbnail
近期的「貼文發佈流程 & 版型大更新」功能大家使用了嗎? 新版式整體視覺上「更加凸顯圖片」,為了搭配這次的更新,我們推出首次貼文策展 ❤️ 使用貼文功能並完成這次的指定任務,還有機會獲得富士即可拍,讓你的美好回憶都可以用即可拍珍藏!
Thumbnail
最近身旁有幾位正在懷孕、或剛生產完的朋友,讓我想起自己在懷孕期間印象最深刻的三件「怪事」,其中又以第三件事最誇張。
Thumbnail
不知道大家在買房之前是不是都會參考親朋好友的意見,或是上網看一些買房注意事項,有時候考慮了這塊就忘了那塊,考慮的那塊又忘了這塊.......
Thumbnail
塔西佗陷阱(Tacitus Trap),一個得名於古羅馬歷史學家塔西佗的政治學理論,意指倘若公權力失去其公信力,無論如何發言或是處事,社會均將給予其負面評價。 當然信著恆信、不信者恆不信,這就是真實的人生。
Thumbnail
關於片名   台灣片名《花漾女子》,原文片名《Promising Young Woman》,台灣譯名將時間定格在悲劇發生前,而原文片名則進一步帶我們看見另一個可能性結果
Thumbnail
前幾年因為身體的關係,當了幾年的律師逃兵,當時開了之前的事務所以後,一時間也沒有特別想要做甚麼事情,所以就邊讀一點書、早晚運動一下,剛好聽到當年同梯朋友進去金融業工作,因此也抱著嘗(ㄊㄠˊ)試(ㄅㄧˋ)的心態,找了份銀行法令遵循的工作
Thumbnail
那天我問隊友,怎樣才算是一部小說呢?按字數計算嗎? 他說:「故事內起承轉合都有,就算」 所以我大膽地按著他的標準,將自己寫過的故事,粗略整理出一個明細。
纏中說禪,本名李彪,專欄筆名木子,其人是中國股市比較早期的操盤手,所以他比較熟悉a股的市場情況。他以“纏中說禪”為筆名,從2002年開始寫博客,直到2008年癌症病重停更,期間寫下了不少文章。而博客文章中最為著名的就是他的“教你炒股票”系列文章,他在這個系列裡講到的炒股理論和方法被粉絲稱為“纏論
Thumbnail
婚姻是人生大事,對溥儀尤其如此,因為如果皇帝大婚,就代表溥儀可以脫離眾多便宜老媽的束縛而得以親政。 但詭異的是,這個可以讓他脫離便宜老媽掌控的婚姻,卻還是要由便宜老媽進行主導並且居中角力......
Thumbnail
上次我提到:溥儀就是個死小孩。其實這不能全怪溥儀,而要怪詭異的宮廷教育及生活制度......
Thumbnail
近期電影「末代皇帝」重新修復上映。 為了推坑這部經典之作,本人決定以溥儀本身的自傳《我的前半生》為主要基底,和大家談一些電影中礙於篇幅或是藝術改編,而不容易察覺或是沒有呈現的真實歷史。
Thumbnail
1.加權指數與櫃買指數 週五的加權指數在非農就業數據開出來後,雖稍微低於預期,但指數仍向上噴出,在美股開盤後於21500形成一個爆量假突破後急轉直下,就一路收至最低。 台股方面走勢需觀察週一在斷頭潮出現後,週二或週三開始有無買單進場支撐,在沒有明確的反轉訊號形成前,小夥伴盡量不要貿然抄底,或是追空
Thumbnail
近期的「貼文發佈流程 & 版型大更新」功能大家使用了嗎? 新版式整體視覺上「更加凸顯圖片」,為了搭配這次的更新,我們推出首次貼文策展 ❤️ 使用貼文功能並完成這次的指定任務,還有機會獲得富士即可拍,讓你的美好回憶都可以用即可拍珍藏!
Thumbnail
最近身旁有幾位正在懷孕、或剛生產完的朋友,讓我想起自己在懷孕期間印象最深刻的三件「怪事」,其中又以第三件事最誇張。
Thumbnail
不知道大家在買房之前是不是都會參考親朋好友的意見,或是上網看一些買房注意事項,有時候考慮了這塊就忘了那塊,考慮的那塊又忘了這塊.......
Thumbnail
塔西佗陷阱(Tacitus Trap),一個得名於古羅馬歷史學家塔西佗的政治學理論,意指倘若公權力失去其公信力,無論如何發言或是處事,社會均將給予其負面評價。 當然信著恆信、不信者恆不信,這就是真實的人生。
Thumbnail
關於片名   台灣片名《花漾女子》,原文片名《Promising Young Woman》,台灣譯名將時間定格在悲劇發生前,而原文片名則進一步帶我們看見另一個可能性結果
Thumbnail
前幾年因為身體的關係,當了幾年的律師逃兵,當時開了之前的事務所以後,一時間也沒有特別想要做甚麼事情,所以就邊讀一點書、早晚運動一下,剛好聽到當年同梯朋友進去金融業工作,因此也抱著嘗(ㄊㄠˊ)試(ㄅㄧˋ)的心態,找了份銀行法令遵循的工作
Thumbnail
那天我問隊友,怎樣才算是一部小說呢?按字數計算嗎? 他說:「故事內起承轉合都有,就算」 所以我大膽地按著他的標準,將自己寫過的故事,粗略整理出一個明細。
纏中說禪,本名李彪,專欄筆名木子,其人是中國股市比較早期的操盤手,所以他比較熟悉a股的市場情況。他以“纏中說禪”為筆名,從2002年開始寫博客,直到2008年癌症病重停更,期間寫下了不少文章。而博客文章中最為著名的就是他的“教你炒股票”系列文章,他在這個系列裡講到的炒股理論和方法被粉絲稱為“纏論
Thumbnail
婚姻是人生大事,對溥儀尤其如此,因為如果皇帝大婚,就代表溥儀可以脫離眾多便宜老媽的束縛而得以親政。 但詭異的是,這個可以讓他脫離便宜老媽掌控的婚姻,卻還是要由便宜老媽進行主導並且居中角力......
Thumbnail
上次我提到:溥儀就是個死小孩。其實這不能全怪溥儀,而要怪詭異的宮廷教育及生活制度......
Thumbnail
近期電影「末代皇帝」重新修復上映。 為了推坑這部經典之作,本人決定以溥儀本身的自傳《我的前半生》為主要基底,和大家談一些電影中礙於篇幅或是藝術改編,而不容易察覺或是沒有呈現的真實歷史。