[Android] 關於 Model 層的幾點思考(一)

前言

Android 開發過程當中,Model 層一般是比較薄弱的。獲取數據的代碼通過各類優秀的封裝,已經能夠簡化到短短几行代碼,對於簡單的項目而言,全都寫在 Activity/Fragment 中就是最合適的了,若是使用了 MVP 或者 MVVM 模式,也基本會把數據的獲取放在 Presenter/ViewModel 中。(後面用業務邏輯層表示 Controller/Presenter/ViewModel)android

但 Model 層是很重要的,MVC,MVP,MVVM 甚至更復雜的架構模式,都須要 Model 層。最近爲了作接口數據格式的自動化測試,又對 Model 層的實現進行了一次學習,本文記錄了學習過程當中的一些問題以及我的理解。問題以下:git

  • 爲何數據獲取不該該寫在業務邏輯裏?
  • Model 層應該包括什麼內容?
  • 如何構造 Model 層?
  • 如何以 Model 爲界線,分別對業務層和數據層作自動化測試?

問題一:爲何數據獲取不該該寫在業務邏輯裏?

說來慚愧,以前寫過的代碼都是在業務裏作網絡請求,處理回調數據。這種方法存在幾個問題:github

  1. 多個頁面請求同一接口時,處理代碼會重複
  2. 添加數據緩存功能會影響業務代碼
  3. 沒法進行單元測試

而獨立出 Model 層能夠解決上述問題,使代碼更易讀且便於擴展,還能添加單元測試提升軟件質量,對於 App 的長期發展有很大的好處。數據庫

問題二:Model 層應該包括什麼內容?

  1. 數據獲取的方法:包括網絡請求和讀取本地文件、數據庫等
  2. 數據處理的方法:Model 層提供的數據應該是業務中直接可用的,這樣就能在測試中區分開錯誤的來源是數據仍是邏輯 bug
  3. 實體類

問題三:如何構造 Model 層?

終於到了寫代碼的時間,此次參考的依然是 googlesamples/android-architecture。爲了知足單元測試的需求,我將一個 demo 項目重構爲 MVP 模式了。緩存

Model 層內部還須要再分層,將不一樣來源(網絡和本地)的數據獲取代碼分開。代碼的結構大概這樣:bash

類結構圖

業務層須要的數據獲取定義成 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 中處理緩存邏輯:ide

//
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 年了,我居然還沒寫過單元測試,實際上是很無奈的一件事。不寫測試的理由可能有不少,但寫單元測試的理由只有爲了更高的代碼質量。爲了讓代碼可以長期維護下去,解耦和單元測試都是很是重要的。

那麼你開始寫單元測試了嗎?

相關文章
相關標籤/搜索