前言
Android 開發過程中,Model 層通常是比較薄弱的。獲取資料的程式碼經過各種優秀的封裝,已經可以簡化到短短几行程式碼,對於簡單的專案而言,全都寫在 Activity/Fragment 中就是最合適的了,如果使用了 MVP 或者 MVVM 模式,也基本會把資料的獲取放在 Presenter/ViewModel 中。(後面用業務邏輯層表示 Controller/Presenter/ViewModel)
但 Model 層是很重要的,MVC,MVP,MVVM 甚至更復雜的架構模式,都需要 Model 層。最近為了做介面資料格式的自動化測試,又對 Model 層的實現進行了一次學習,本文記錄了學習過程中的一些問題以及個人理解。問題如下:
- 為什麼資料獲取不應該寫在業務邏輯裡?
- Model 層應該包括什麼內容?
- 如何構造 Model 層?
- 如何以 Model 為界線,分別對業務層和資料層做自動化測試?
問題一:為什麼資料獲取不應該寫在業務邏輯裡?
說來慚愧,之前寫過的程式碼都是在業務裡做網路請求,處理回撥資料。這種方法存在幾個問題:
- 多個頁面請求同一介面時,處理程式碼會重複
- 新增資料快取功能會影響業務程式碼
- 無法進行單元測試
而獨立出 Model 層可以解決上述問題,使程式碼更易讀且便於擴充套件,還能新增單元測試提高軟體質量,對於 App 的長期發展有很大的好處。
問題二:Model 層應該包括什麼內容?
- 資料獲取的方法:包括網路請求和讀取本地檔案、資料庫等
- 資料處理的方法:Model 層提供的資料應該是業務中直接可用的,這樣就能在測試中區分開錯誤的來源是資料還是邏輯 bug
- 實體類
問題三:如何構造 Model 層?
終於到了寫程式碼的時間,這次參考的依然是 googlesamples/android-architecture。為了滿足單元測試的需求,我將一個 demo 專案重構為 MVP 模式了。
Model 層內部還需要再分層,將不同來源(網路和本地)的資料獲取程式碼分開。程式碼的結構大概這樣:
業務層需要的資料獲取定義成 DataSource 介面中的函式,固定引數和回撥。具體實現為 LocalDataSource 和 RemoteDataSource 等。Repository 實現 DataSource 並持有具體的一種或多種 DataSource,通過組合不同的 DataSource 實現獲取資料的功能。
舉個栗子:
某 App 首頁需要通過網路獲取一個列表資料來展示,為了更好的使用者體驗,每次重新整理的列表會快取在本地。這樣在請求成功前就不是空白的頁面了。
interface ExDataSource {
interface LoadListCallback{
fun onSuccess(list: ArrayList<ListItemBean>)
fun onError(errorCode: Int, errorMsg: String)
}
fun loadList(page: Int, callback: LoadListCallback)
}
複製程式碼
然後分別實現具體的資料獲取方法:
// 對資料處理的函式可以以靜態方法的方式提到外面
class ExRemoteDataSource: ExDataSource{
override fun loadList(page: Int, callback: LoadListCallback){
// 發起網路請求,解析返回資料,如果不能解析成ArrayList<ListItemBean>也回撥失敗
// 具體程式碼實現與網路請求框架有關,此處不放程式碼了
}
}
···
class ExLocalDataSource: ExDataSource{
override fun loadList(page: Int, callback: LoadListCallback){
// 從資料庫或者檔案獲取快取的資料
}
fun setCacheList(list: ArrayList<ListItemBean>){
// 更新快取內容
}
}
複製程式碼
最後在 Repository 中處理快取邏輯:
//
class ExRepositiry(
private val remoteDataSource: ExRemoteDataSource,
private val localDataSource: ExLocalDataSource
): ExDataSource {
override fun loadList(page: Int, callback: LoadListCallback){
//具體如何使用快取跟需求有關,這裡簡化寫一下
// 先載入本地資料做顯示
localDataSource.loadList(page, callback)
// 同時發起網路請求
remoteDataSource.loadList(page, object: LoadListCallback{
override fun onSuccess(list: ArrayList<ListItemBean>){
// 成功後更新快取,重新整理頁面
localDataSource.setCacheList(list)
callback.onSuccess(list)
}
override fun onError(errorCode: Int, errorMsg: String){
callback.onError(errorCode, errorMsg)
}
})
}
}
複製程式碼
業務層只需要建立 Repository 就能獲得想要的資料了,對於錯誤的情況,就詳細規劃 onError 的回撥,再根據具體需求處理。
- 問題3.1:如何避免建立大量回撥介面?
介面回撥是無法從根本上取代的,如果為了程式碼簡明,可以建立幾個泛型介面模板來避免每個請求對應一個介面。使用 Kotlin 的話可以直接按成功和失敗傳入函式:
···
fun loadList(
page: Int,
onSuccess: (ArrayList<ListItemBean>) -> Unit,
onError: (errorCode: Int, errorMsg: String) -> Unit
)
···
複製程式碼
- 問題3.2:如何劃分 Repository?
隨著專案的發展,需要的資料會越來越多,都寫在同一個 Repository 中獲取資料會讓程式碼的可讀性下降。應該按照業務將 Repository 模組化,以適應未來專案的模組化和元件化。(不需要分得太細碎,Repository 本身由多個獨立的資料獲取程式碼構成,即使有很多行也能保證邏輯清晰)
問題四:如何以 Model 為界線,分別對業務層和資料層做自動化測試?
算了一下內容,再寫下去就太長了。而且關於單元測試的部分還沒應用到專案中,不確定還有沒有坑,最後這個問題下週單獨寫一篇吧。
總結
Model 層可以說是欠了很久的技術債了,最初覺得只是幾行程式碼的網路請求拆出來也沒有意義,隨著業務的發展,出現了很多需要快取的頁面,就也把取本地的資料的程式碼寫在業務邏輯中了。現在要保證複雜邏輯程式碼的穩定性,想要新增單元測試,再回頭看程式碼才明白已經走偏了太多。
程式碼的架構應該是分層,而不是分塊。很多程式碼中把一部分業務邏輯委託到一個 xxManager 去做,表面上似乎單個檔案中的程式碼少了,但並不符合單一職責原則,實際上程式碼的可讀性還是不好,後續維護也依然麻煩。
從事 Android App 開發快 2 年了,我竟然還沒寫過單元測試,其實是很無奈的一件事。不寫測試的理由可能有很多,但寫單元測試的理由只有為了更高的程式碼質量。為了讓程式碼能夠長期維護下去,解耦和單元測試都是非常重要的。
那麼你開始寫單元測試了嗎?