前幾天一直在看 State 和 Lifecycle,今天想先換個方向,回頭看以前自己寫 Android 時很自然就放進架構裡的一層:
Repository。
以前寫 MVVM,很容易整理出這樣的結構:
Fragment
↓
ViewModel
↓
Repository
↓
DAO
↓
Room
看起來很合理,也是很常見的分層方式。
但如果真的打開 Repository,卻有可能看到這種 Code:
class RecordRepository(
private val dao: RecordDao
) {
fun getRecords() = dao.getRecords()
suspend fun insert(record: Record) {
dao.insert(record)
}
suspend fun delete(record: Record) {
dao.delete(record)
}
}
看完會出現一個很直接的問題:
如果 Repository 只是把 DAO 的 Function 再寫一次,那我為什麼不直接在 ViewModel 用 DAO?
假設 ViewModel 原本是:
repository.getRecords()
Repository 裡面則是:
dao.getRecords()
最後其實只是:
ViewModel
↓
Repository.getRecords()
↓
DAO.getRecords()
Repository 沒有做任何判斷,也沒有隱藏什麼複雜度。
如果只是為了讓架構長成:
UI
↓
ViewModel
↓
Repository
↓
Data Source
就機械式地多包一層,那這層 abstraction 的價值確實值得懷疑。
所以 Repository 的意義應該不是:
ViewModel 和 DAO 中間一定要隔一個 Class。
而是要看:
它到底替上層隱藏了什麼?
像自己的 Timer Project,紀錄主要存在 Room。
如果需求很單純:
我要所有紀錄
↓
Room
那 Repository 很可能真的只剩:
fun getRecords() = dao.getRecords()
這時候它的價值並不明顯。
但假設之後資料來源開始變成:
Room
API
Cache
問題就不一樣了。
ViewModel 想要的其實只是:
我要目前的紀錄。
它不應該每次都要知道:
先讀 Room 嗎?
還是先打 API?
API 成功後要不要寫回 Room?
沒有網路時要用哪份?
Cache 過期了嗎?
這些已經不是「畫面怎麼顯示」的問題,而是:
資料應該怎麼取得。
這時 Repository 就開始有比較明確的責任。
假設今天有:
┌→ API
ViewModel → Repository
└→ Room
ViewModel 可以只知道:
repository.getRecords()
至於 Repository 裡面可能決定:
先顯示 Room 資料
↓
背景取得最新資料
↓
寫回 Room
↓
Room 更新
↓
UI 收到新資料
ViewModel 不需要知道這些細節。
如果未來資料來源從:
Room
變成:
Room + API
ViewModel 也不一定需要跟著大改。
所以 Repository 真正隔開的其實不是:
ViewModel
和
DAO
而是:
上層想取得什麼資料
和
底層到底怎麼取得資料
這樣看之後,我覺得 DAO 和 Repository 可以用兩個問題區分。
DAO 比較像是在回答:
我要怎麼操作這個 Database?
例如:
@Query("SELECT * FROM record")
fun getRecords(): Flow<List<RecordEntity>>
@Insert
suspend fun insert(record: RecordEntity)
它知道的是:
Table
Query
Insert
Delete
Entity
但 Repository 比較像是在回答:
App 要怎麼取得 Record?
它可能從 Room 取得,也可能從 API 取得,甚至可能同時處理兩邊。
所以:
DAO
→ Database 怎麼操作
Repository
→ Data 怎麼取得
這兩層處理的問題其實不完全一樣。
還有另一個差異。
DAO 可能回傳:
RecordEntity
API 可能回傳:
RecordResponse
但上層真正想使用的可能是:
Record
如果 ViewModel 自己開始處理:
RecordEntity
RecordResponse
API 欄位
Database 欄位
它就會慢慢知道太多 Data Layer 的細節。
這時也可以變成:
Room
↓
RecordEntity
↓
Repository
↓
Record
↓
ViewModel
或:
API
↓
RecordResponse
↓
Repository
↓
Record
↓
ViewModel
上層只面對它真正需要的資料。
這樣未來 Database Schema 或 API Response 改變時,影響範圍也比較容易被限制住。
看到這裡又很容易走向另一個極端:
既然 Repository 是 Data Layer,那所有邏輯都丟進 Repository。
例如:
資料取得
時間格式化
Button 要不要 Enable
Dialog 要不要出現
畫面文字
Navigation
全部塞進去。
這當然也不對。
Repository 的存在不是為了把 ViewModel 變薄而已。
如果某個邏輯其實是在決定:
UI 要怎麼呈現
那它就不會因為「ViewModel 太長」,自動變成 Repository 的責任。
所以判斷一段邏輯該不該進 Repository,我覺得可以先問:
這件事情是在決定「資料怎麼取得」,還是在決定「畫面怎麼呈現」?
這兩個問題的責任並不一樣。
回到最開始的問題。
如果目前真的只有:
ViewModel
↓
Repository
↓
DAO
而 Repository 每個 Function 都只是:
return dao.xxx()
那它確實沒有展現出太多額外價值。
但我現在也不會因此直接得出:
Repository 沒用,刪掉就好。
因為還要看這個 Project 想把哪個邊界固定下來。
例如 ViewModel 如果直接依賴 DAO:
ViewModel
↓
Room DAO
代表上層直接知道:
我的資料來源就是 Room。
而:
ViewModel
↓
Repository
↓
Room
至少保留了一個邊界:
ViewModel 需要的是 Record,不需要直接關心它是不是存在 Room。
所以即使目前 Repository 很薄,它還是可能在表達一個架構上的界線。
只是:
「有邊界」跟「一定值得多一層」不能直接畫上等號。
還是要看 Project 的複雜度。
以前看到:
Fragment
↓
ViewModel
↓
Repository
↓
DAO
很容易覺得層數很完整,所以架構應該比較乾淨。
但現在回頭看,我反而會想知道:
每一層到底負責什麼?
拿掉這一層之後,上下兩層會知道什麼原本不該知道的事情?
它有沒有隱藏一個真正可能改變的細節?
如果完全回答不出來,那可能只是:
為了 abstraction 而 abstraction。
反過來,如果 Repository 真的把:
資料來源
Cache 策略
同步方式
Data Mapping
這些細節隔在 ViewModel 外面,那它存在的理由就很明確。
所以如果今天重新看到:
ViewModel
↓
Repository
↓
DAO
我不會先問:
MVVM 是不是就應該這樣分?
而是會問:
Repository 替 ViewModel 隱藏了什麼?
可能是:
資料來自哪裡
什麼時候更新
Cache 怎麼處理
Database 和 API 怎麼同步
底層資料格式怎麼轉換
如果答案是:
目前什麼都沒有,只是把 DAO Function 再呼叫一次。
那至少我會知道:
現在這個 Repository 的主要價值可能只是建立邊界,而不是因為它本身承擔了很多邏輯。
這也讓我重新理解「分層」這件事。
架構不是:
Layer 越多
↓
越乾淨
真正重要的應該是:
每一層都有明確責任
↓
改變發生時
↓
影響可以被限制在合理範圍
一層 abstraction 值不值得存在,不是看架構圖上有沒有它,而是它到底替上層隱藏了什麼。
Repository 也是一樣。
它不應該只是因為「MVVM 好像都會有」才存在,而是當資料來源和取得方式開始變複雜時,上層仍然可以只專心問:
我要什麼資料?
至於資料到底從哪裡來,才是 Repository 真正開始有價值的地方。