Android 的 Repository Pattern 到底是什麼?

YANBIN HUNG
10 min readMay 11, 2020

--

今天在臉書的社群中看到了一個問題:

想請問在使用repository pattern的時候

在repository做網路請求後,應該要在repository解析資料然後返回livedata給viewModel比較好還是在viewModel解析資料然後返回資料給view呢?

這問題其實非常有趣,想當初我第一次看到 Repository 這個關鍵字的時候,是從 Google Sample code 看到的。當時心中的想法有點像是,這是什麼東西?在什麼時候使用的?看起來有點像是拿資料用的?於是就興起了想去好好了解他的念頭,但是礙於當時的經驗不足,看完了還是無法全面的掌握 Repository 的使用時機。

經過這幾年的實戰經驗以及閱讀相關書籍,對於 Repository 有了不同程度的理解,思考的切入點從單維擴充到多維,並在各方面的考量之下去取得平衡,在此分享一下目前為止我對 Repository pattern 的看法以及心得。

Context

Context 是非常重要的,當你在說 Repository 這個字的當下,是處於哪種情境呢?這個將會決定你在這上面花的心力是多還是少。

  1. 學習:這個是也是我一開頭描述的情境,基於某種原因,不管是閱讀程式碼還是建立自己的新專案。這時候你將會閱讀相關資料(可能也包含本篇),經過多方嘗試,最後整理出你的結論。但是要記住,這時候得到的結論很有可能會被翻盤的,非常重要的一點是,這不是什麼大問題,一切都是學習的過程。切記不要堅持某種論述,認為 Repository pattern 有一種 “最正確” 的使用方式,把時間都浪費在與別人爭論上。
  2. 設計架構:在這種情境底下,要考慮的事非常多。要想想 Repository 存在的必要性:他有多少職責、整個團隊是否跟你有一樣的認知、維護成本是高還是低、整個專案的複雜度是高還是低、整個專案是否有不同定位的 Repository。
  3. 開發 Feature :在這種情境下就不太需要考慮太多了,有可能你只是需要一個存放資料的地方,Repository 又是大家所熟悉的名詞,就使用它了。有可能你的專案在一個需要快速交付的階段,花很多心力在釐清職責上可不會讓你的產品價值有顯著的差異(短時間內)。

導入模式

很多的模式(或是框架、架構、model)都會提到 Repository 這個名詞,他們在使用這個名詞的同時,所想要表達的意圖是不太一樣的,給予他的定位也不一樣,在這裏提供以下我學習過的模式:

1. MVX Pattern

MVC, MVP, MVVM 這些在 Android 領域已經是講到快爛掉的東西,那麼在這個模式底下,Repository 應該會是在哪邊呢?又要如何使用?

典型的 3-tier architecture

不管是哪一種 MVX ,主要的核心概念都脫離不了這三層,Presentation Layer 用來顯示 UI ,Domain Layer 用來處理核心領域以及商業邏輯,(Data) Access Layer 用來獲取資料。在這種模式底下,Repository 很理所當然的會在 Access Layer ,那 Repository 需要 interface 跟具體實作分開嗎?不一定,在這樣的模式底下這不會是需要關注的點,只需要知道他是用來獲取資料的即可。那 Repository 要拿網路資料還是 DB 資料?也不一定,原因跟上面一樣,重點是資料,來源不是關注點。

2. Clean Architecture

Clean Architecture 非常注重核心領域層,內層不應該知道外層的任何資訊,因為這樣,核心領域層才可以專注在處理商業需求。DB 以及 Network 的實作細節都不會知道,為了限制這點,甚至有時會嚴格限定模組之間的依賴關係,一個根本就無法 import 的類別,也就無法有機會使用到它。

在這種模式底下,Repository 就會放在 Use case port 這個地方,他被用來隔絕外面的世界,只提供了一個公開的介面可以給 Use case interactor 使用,沒有任何實作。而在 Gateways 層會提供該 Repository 的實作,該實作將會知道如何使用各個地方來的資料來做整合。

請注意,在這邊綠色跟藍色之間還有隔另外一層,也就是說 Repository 的實作是使用另外一個公開介面來拿資料,而實際上並不認識 DB。但是通常我會省去這一層,對大部分我的案例來說這有點多餘。

3. Domain Driven Design

大概說明一下,Domain Driven Design 是用來解決複雜的商業問題的。在這邊所注重的將會是 Model 本身,一個複雜的系統會需要一個富有表達性的模型來描述各個元件之間的相互關係。因此管理這樣的模型也會是很重要的一件事。模型跟模型之間有時候可以組合在一起成為了一個聚合 (Aggregate),舉例來說,一個家庭式公寓的模型底下可能會有很多房間的模型,當我要設計一個系統來管理家庭式公寓這樣的 Aggregate 時,Repository 就要讓我可以好好的儲存以及修改,以及不要出錯。

所以這邊的關注點會是 Repository 的公開介面是否是 Domain Service 正好需要的?要一個可以新增多個資料的介面?還是只能一次新增一個資料?使用這些公開介面是否能正確表達領域知識?還是不小心参進了一些技術細節呢?

4. Design pattern

以設計模式的情況下,我這邊的切入點比較會像是:Separation of concerns。設計這個 pattern 的意圖,有很大的一部分是要進行資料的抽象化,如此一來使用 Repository 的另一端就不需要知道背後的實作,並且可以方便的對 Repository 進行抽換。不管使用 Repository 的是 ViewModel ,Use case 還是 View ,都能藉由 Separation of concern 而得到好處。

資料型別

隨著應用程式越來越複雜,資料的型別也有可能越來越多元,其用途也會不一樣,這一節會討論一下 Repository 應該使用怎麼樣的資料型別 (也就是本文開頭的“解析”問題)

以下描述的資料型別,都是指 Repository “介面“上所會看到的資料,例如下方的 Repository 對應的 User

interface UserRepository {
fun addUser(user: User)
fun getAll(): List<User>
}

1. Raw data

其實這種情形應該不太會出現,但還是提一下,因為有說到了“解析”這件事,“解析”有可能是從 Json String 轉化成 Json Model ,也有可能是 Json Model 轉化成 Domain Model 。如果是 Json String 的話就是 Raw data 了。遇到這種情形,其實已經很難稱上這是一個 Repository ,因為基本的模型抽象化也沒做到,跟上面的 design pattern 的描述不符合。這時建議可以直接改名為 NetworkService 或是 ApiService 畢竟這樣的描述比較符合正在做的事。

2. Json Model

這是蠻常見的一種資料類型,通常後端會定義好 Json 的格式,Mobile 這邊可以利用手邊的工具轉成物件(例如 Gson)。然後 Repository 直接使用這樣的 Model 來給呼叫方使用。適合用在做簡單的 Api 串接 App ,很多東西打打 Api 就可以直接拿來顯示在畫面上了,不需要經過太多邏輯判斷。

3. Database Model

這也是常見的類型之一,最近很多人應該都開始用 Room 這套函式庫了,其中他定義 Table 的方式非常簡單,只要將一個物件指定為 Entity 就行了。適合用來做本地儲存類型的 App ,或是 google 很推的 Single source of truth(可參考這邊),但是缺點會是 Model 包含了一些資料庫實作細節,讓這個 Model 本身的職責不單一。

4. Domain Model

這種 Model 是符合 Clean Architecture 以及 Domain Driven Design 的。透過這個更加精煉的模型,看到這個模型的人就會意識到這個 Model 本身所代表的意義以及使用方式,並且與核心的商業需求目標一致。通常會用在複雜程度夠高的 App ,產品的商業邏輯放在 Mobile 比重很高,無法將這段邏輯放在 Server 上(或是沒有 Server)時使用的。

這個 Model 可能會跟 Database model 或是 Json model 有點類似但是又不完全一樣,這是無法避免的,在複雜度還不夠高時會發生。但隨著時間演進,你的領域知識跟實作細節越差越遠時,就能體會這種做法的好處。例如我有遇過非常“巢”的 Json 資料格式,但是我想要的資料其實從第三層看就好,這時使用 Domain Model 就不需要像 Json Model 一樣需要透過三個物件(像是aObject.bObject.c)才能拿到。

還有另一個例子是,在公司的產品中,我們實作“剪裁”這個功能可以有三種不同的方式,第一個是使用一連串的點,第二個是使用遮罩,第三個是使用 svg 。這三種在 Json Model 中都有不同欄位來表示他們的意義,但是後來我們發現,同時間只會有其中一個剪裁發生,不會同時有兩種。而且其實 Domain Model 只要用“剪裁”這個觀念來設計 model 就可以省下我們非常多的重工(像是檢查 null 跟一堆的 && 還有 ||)。

實作

在 Repository 的實作上可以有很多不同的可能,其中有些在前面已經提過了,但是這邊的目的是想要以實作這個角度來做切入。

  1. InMemory Repository
  2. Fake Repository
  3. Database Repository
  4. Api Repository
  5. Merged Repository

InMemory Repository 是指,其中的實作都是 Java 的資料結構實現的,例如 HashMap ,insert 資料就等於 HashMap.put ,query 資料就等於 HashMap.get。通常會在其他 Team 的資源還沒 Ready 的情況下使用,例如後端 Api 還沒好,那你就可以先依照需求設計 Model ,可以直接用來接畫面,而且要替換資料也非常簡單,可以自己寫死,到時後端做好時, Mobile 這邊只需要把資料轉換好就好。

Fake Repository ,用在單元測試的時候可以用來假造預設值,有時候會比使用 Mock 框架來得更直覺,而且可以重用。第三跟第四的前面已經有介紹過了就不加贅述了。

Merged Repository ,根據需求去合併不同來源的資料,來源有可能是 database, network, asset, file 等等,但是要特別注意的是資料的同步問題,關於這個問題,可以去參考 Single source of truth 相關的討論。

總結

前面說了這麼多面向,那麼提問的人所需要的解決方案是什麼呢?其實沒有標準答案,因為這個問題本身提供的 Context 還不夠多,可以做的方式有很多種。如果問題像是:我在“學習”使用 “Clean Architecture” ,那麼Repository 會需要負責解析成 “Json Model” 嗎?像這樣的提問就有提供非常多線索,答案就可以限縮到一定的範圍。

這邊也要說明一下,沒有要批評提問者的意思,因為如果問得出這麼“精確”的問題的話,那麼提問者搞不好已經想出答案了。剛好也趁這個機會,整理一下我目前為止的知識所能想到的面向(Context、模式、資料型別、實作),將來也會修正也不一定。不知道讀者看了之後,是否想到了另一個我沒注意到的面向呢?有的話請一定要跟我分享!

--

--