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

万俟霜風發表於2019-04-03

前言

Android 開發過程中,Model 層通常是比較薄弱的。獲取資料的程式碼經過各種優秀的封裝,已經可以簡化到短短几行程式碼,對於簡單的專案而言,全都寫在 Activity/Fragment 中就是最合適的了,如果使用了 MVP 或者 MVVM 模式,也基本會把資料的獲取放在 Presenter/ViewModel 中。(後面用業務邏輯層表示 Controller/Presenter/ViewModel)

但 Model 層是很重要的,MVC,MVP,MVVM 甚至更復雜的架構模式,都需要 Model 層。最近為了做介面資料格式的自動化測試,又對 Model 層的實現進行了一次學習,本文記錄了學習過程中的一些問題以及個人理解。問題如下:

  • 為什麼資料獲取不應該寫在業務邏輯裡?
  • Model 層應該包括什麼內容?
  • 如何構造 Model 層?
  • 如何以 Model 為界線,分別對業務層和資料層做自動化測試?

問題一:為什麼資料獲取不應該寫在業務邏輯裡?

說來慚愧,之前寫過的程式碼都是在業務裡做網路請求,處理回撥資料。這種方法存在幾個問題:

  1. 多個頁面請求同一介面時,處理程式碼會重複
  2. 新增資料快取功能會影響業務程式碼
  3. 無法進行單元測試

而獨立出 Model 層可以解決上述問題,使程式碼更易讀且便於擴充套件,還能新增單元測試提高軟體質量,對於 App 的長期發展有很大的好處。

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

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

問題三:如何構造 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 年了,我竟然還沒寫過單元測試,其實是很無奈的一件事。不寫測試的理由可能有很多,但寫單元測試的理由只有為了更高的程式碼質量。為了讓程式碼能夠長期維護下去,解耦和單元測試都是非常重要的。

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

相關文章