我是怎麼把業務代碼越寫越複雜的 | MVP - MVVM - Clean Architecture

本文以一個真實項目的業務場景爲載體,描述了經歷一次次重構後,代碼變得愈來愈複雜(you ya)的過程。java

本篇 Demo 的業務場景是:從服務器拉取新聞並在列表展現。android

GodActivity

剛接觸 Android 時,我是這樣寫業務代碼的(省略了和主題無關的 Adapter 和 Api 細節):git

class GodActivity : AppCompatActivity() {
 private var rvNews: RecyclerView? = null  private var newsAdapter = NewsAdapter()   // 用 retrofit 拉取數據  private val retrofit = Retrofit.Builder()  .baseUrl("https://api.apiopen.top")  .addConverterFactory(MoshiConverterFactory.create())  .client(OkHttpClient.Builder().build())  .build()  private val newsApi = retrofit.create(NewsApi::class.java)   // 數據庫操做異步執行器  private var dbExecutor = Executors.newSingleThreadExecutor()   override fun onCreate(savedInstanceState: Bundle?) {  super.onCreate(savedInstanceState)  setContentView(R.layout.news_activity)  initView()  fetchNews()  }   private fun initView() {  rvNews = findViewById(R.id.rvNews)  rvNews?.layoutManager = LinearLayoutManager(this)  }   // 列表展現新聞  private fun showNews(news : List<News>) {  newsAdapter.news = news  rvNews?.adapter = newsAdapter  }   // 獲取新聞  private fun fetchNews() {  // 1. 先從數據庫讀老新聞以快速展現  queryNews().let{ showNews(it) }  // 2. 再從網絡拉新聞替換老新聞  newsApi.fetchNews(  mapOf("page" to "1","count" to "4")  ).enqueue(object : Callback<NewsBean> {  override fun onFailure(call: Call<NewsBean>, t: Throwable) {  Toast.makeText(this@GodActivity, "network error", Toast.LENGTH_SHORT).show()  }   override fun onResponse(call: Call<NewsBean>, response: Response<NewsBean>) {  response.body()?.result?.let {  // 3. 展現新新聞  showNews(it)  // 4. 將新聞入庫  dbExecutor.submit { insertNews(it) }  }  }  })  }   // 從數據庫讀老新聞(僞代碼)  private fun queryNews() : List<News> {  val dbHelper = NewsDbHelper(this, ...)  val db = dbHelper.getReadableDatabase()  val cursor = db.query(...)  var newsList = mutableListOf<News>()  while(cursor.moveToNext()) {  ...  newsList.add(news)  }  db.close()  return newsList  }   // 將新聞寫入數據庫(僞代碼)  private fun insertNews(news : List<News>) {  val dbHelper = NewsDbHelper(this, ...)  val db = dbHelper.getWriteableDatabase()  news.foreach {  val cv = ContentValues().apply { ... }  db.insert(cv)  }  db.close()  } } 複製代碼

畢竟當時的關注點是實現功能,首要解決的問題是「如何繪製佈局」、「如何操縱數據庫」、「如何請求並解析網絡數據」、「如何將數據填充在列表中」。待這些問題解決後,也沒時間思考架構,因此就產生了上面的God Activity。Activity 管的太多了!Activity 知道太多細節:github

  1. 異步細節
  2. 訪問數據庫細節
  3. 訪問網絡細節
  1. 若是大量 「細節」 在同一個層次被鋪開,就顯得囉嗦,增長理解成本。

拿說話打個比方:web

你問 「晚飯吃了啥?」數據庫

「我用勺子一口一口地吃了雞生下的蛋和番茄再加上油一塊兒炒的菜。」編程

聽了這樣地回答,你還會和他作朋友嗎?其實你並不關心他吃的工具、吃的速度、食材的來源,以及烹飪方式。api

  1. 「細節」 相對的是 「抽象」,在編程中 「細節」 易變,而 「抽象」 相對穩定。

好比 「異步」 在 Android 中就有好幾種實現方式:線程池、HandlerThread、協程、IntentServiceRxJava緩存

  1. 「細節」 增長耦合。

GodActivity 引入了大量本和它無關的類:RetrofitExecutorsContentValuesCursorSQLiteDatabaseResponseOkHttpClient。Activity 本應該只和界面展現有關。服務器

將界面展現和獲取數據分離

既然 Activity 知道太多,那就讓Presenter來爲它分擔:

// 構造 Presenter 時傳入 view 層接口 NewsView
class NewsPresenter(var newsView: NewsView): NewsBusiness {  private val retrofit = Retrofit.Builder()  .baseUrl("https://api.apiopen.top")  .addConverterFactory(MoshiConverterFactory.create())  .client(OkHttpClient.Builder().build())  .build()   private val newsApi = retrofit.create(NewsApi::class.java)   private var executor = Executors.newSingleThreadExecutor()   override fun fetchNews() {  // 將數據庫新聞經過 view 層接口通知 Activity  queryNews().let{ newsView.showNews(it) }  newsApi.fetchNews(  mapOf("page" to "1", "count" to "4")  ).enqueue(object : Callback<NewsBean> {  override fun onFailure(call: Call<NewsBean>, t: Throwable) {  newsView.showNews(null)  }   override fun onResponse(call: Call<NewsBean>, response: Response<NewsBean>) {  response.body()?.result?.let {  // 將網絡新聞經過 view 層接口通知 Activity  newsView.showNews(it)  dbExecutor.submit { insertNews(it) }  }  }  })  }   // 從數據庫讀老新聞(僞代碼)  private fun queryNews() : List<News> {  // 經過 view 層接口獲取 context 構造 dbHelper  val dbHelper = NewsDbHelper(newsView.newsContext, ...)  val db = dbHelper.getReadableDatabase()  val cursor = db.query(...)  var newsList = mutableListOf<News>()  while(cursor.moveToNext()) {  ...  newsList.add(news)  }  db.close()  return newsList  }   // 將新聞寫入數據庫(僞代碼)  private fun insertNews(news : List<News>) {  val dbHelper = NewsDbHelper(newsView.newsContext, ...)  val db = dbHelper.getWriteableDatabase()  news.foreach {  val cv = ContentValues().apply { ... }  db.insert(cv)  }  db.close()  } } 複製代碼

無非就是複製 + 粘貼,把 GodActivity 中的「異步」、「訪問數據庫」、「訪問網絡」、放到了一個新的Presenter類中。這樣 Activity 就變簡單了:

class RetrofitActivity : AppCompatActivity(), NewsView {
 // 在界面中直接構造業務接口實例  private val newsBusiness = NewsPresenter(this)   private var rvNews: RecyclerView? = null  private var newsAdapter = NewsAdapter()   override fun onCreate(savedInstanceState: Bundle?) {  super.onCreate(savedInstanceState)  setContentView(R.layout.news_activity)  initView()  // 觸發業務邏輯  newsBusiness.fetchNews()  }   private fun initView() {  rvNews = findViewById(R.id.rvNews)  rvNews?.layoutManager = LinearLayoutManager(this)  }   // 實現 View 層接口以更新界面  override fun showNews(news: List<News>?) {  newsAdapter.news = news  rvNews?.adapter = newsAdapter  }   override val newsContext: Context  get() = this } 複製代碼

Presenter的引入還增長了通訊成本:

interface NewsBusiness {
 fun fetchNews() } 複製代碼

這是MVP模型中的業務接口,描述的是業務動做。它由Presenter實現,而界面類持有它以觸發業務邏輯。

interface NewsView {
 // 將新聞傳遞給界面  fun showNews(news:List<News>?)  // 獲取界面上下文  abstract val newsContext:Context } 複製代碼

MVP模型中,這稱爲View 層接口Presenter持有它以觸發界面更新,而界面類實現它以繪製界面。

這兩個接口的引入,意義非凡:

接口把 作什麼(抽象) 和 怎麼作(細節) 分離。這個特性使得 關注點分離 成爲可能:接口持有者只關心 作什麼,而 怎麼作 留給接口實現者關心。

Activity 持有業務接口,這使得它不須要關心業務邏輯的實現細節。Activity 實現View 層接口,界面展現細節都內聚在 Activity 類中,使其成爲MVP中的V

Presenter 持有View 層接口,這使得它不須要關心界面展現細節。Presenter 實現業務接口,業務邏輯的實現細節都內聚在 Presenter 類中,使其成爲MVP中的P

這樣作最大的好處是下降代碼理解成本,由於不一樣細節再也不是在同一層次被鋪開,而是被分層了。閱讀代碼時,「淺嘗輒止」或「不求甚解」的閱讀方式極大的提升了效率。

這樣作還能縮小變動成本,業務需求發生變動時,只有Presenter類須要改動。界面調整時,只有V層須要改動。同理,排查問題的範圍也被縮小。

這樣還方便了自測,若是想測試各類臨界數據產生時界面的表現,則能夠實現一個PresenterForTest。若是想覆蓋業務邏輯的各類條件分支,則能夠方便地給Presenter寫單元測試(和界面隔離後,Presenter 是純 Kotlin 的,不含有任何 Android 代碼)。

NewsPresenter也不單純!它除了包含業務邏輯,還包含了訪問數據的細節,應該用一樣的思路,抽象出一個訪問數據的接口,讓Presenter持有,這就是MVP中的M。它的實現方式能夠參考下一節的Repository

數據視圖互綁 + 長生命週期數據

即便將訪問數據的細節剝離出Presenter,它依然不單純。由於它持有View 層接口,這就要求Presenter需瞭解 該把哪一個數據傳遞給哪一個接口方法,這就是 數據綁定,它在構建視圖時就已經肯定(無需等到數據返回),因此這個細節能夠從業務層剝離,歸併到視圖層。

Presenter的實例被 Activity 持有,因此它的生命週期和 Activiy 同步,即業務數據和界面同生命週期。在某些場景下,這是一個缺點,好比橫豎屏切換。此時,若是數據的生命週期不依賴界面,就能夠免去從新獲取數據的成本。這勢必 須要一個生命週期更長的對象(ViewModel)持有數據。

生命週期更長的 ViewModel

上一節的例子中,構建Presenter是直接在Activitynew,而構建ViewModel是經過ViewModelProvider.get():

public class ViewModelProvider {
 // ViewModel 實例商店  private final ViewModelStore mViewModelStore;   public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {  // 從商店獲取 ViewModel實例  ViewModel viewModel = mViewModelStore.get(key);   if (modelClass.isInstance(viewModel)) {  return (T) viewModel;  } else {  ...  }  // 若商店無 ViewModel 實例 則經過 Factory 構建  if (mFactory instanceof KeyedFactory) {  viewModel = ((KeyedFactory) (mFactory)).create(key, modelClass);  } else {  viewModel = (mFactory).create(modelClass);  }  // 將 ViewModel 實例存入商店  mViewModelStore.put(key, viewModel);  return (T) viewModel;  } } 複製代碼

ViewModel實例經過ViewModelStore獲取:

// ViewModel 實例商店
public class ViewModelStore {  // 存儲 ViewModel 實例的 Map  private final HashMap<String, ViewModel> mMap = new HashMap<>();   // 存  final void put(String key, ViewModel viewModel) {  ViewModel oldViewModel = mMap.put(key, viewModel);  if (oldViewModel != null) {  oldViewModel.onCleared();  }  }   // 取  final ViewModel get(String key) {  return mMap.get(key);  }   ... } 複製代碼

ViewModelStoreViewModel實例存儲在HashMap中。

ViewModelStore經過ViewModelStoreOwner獲取:

public class ViewModelProvider {
 // ViewModel 實例商店  private final ViewModelStore mViewModelStore;   // 構造 ViewModelProvider 時需傳入 ViewModelStoreOwner 實例  public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {  // 經過 ViewModelStoreOwner 獲取 ViewModelStore   this(owner.getViewModelStore(), factory);  }   public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {  mFactory = factory;  mViewModelStore = store;  } } 複製代碼

ViewModelStoreOwner實例又存儲在哪?

// Activity 基類實現了 ViewModelStoreOwner 接口
public class ComponentActivity extends androidx.core.app.ComponentActivity implements  LifecycleOwner,  ViewModelStoreOwner,  SavedStateRegistryOwner,  OnBackPressedDispatcherOwner {   // Activity 持有 ViewModelStore 實例  private ViewModelStore mViewModelStore;   public ViewModelStore getViewModelStore() {  if (mViewModelStore == null) {  // 獲取配置無關實例  NonConfigurationInstances nc =(NonConfigurationInstances) getLastNonConfigurationInstance();  if (nc != null) {  // 從配置無關實例中恢復 ViewModel商店  mViewModelStore = nc.viewModelStore;  }  if (mViewModelStore == null) {  mViewModelStore = new ViewModelStore();  }  }  return mViewModelStore;  }   // 靜態的配置無關實例  static final class NonConfigurationInstances {  // 持有 ViewModel商店實例  ViewModelStore viewModelStore;  ...  } } 複製代碼

Activity就是ViewModelStoreOwner實例,且持有ViewModelStore實例,該實例還會被保存在一個靜態類中,因此ViewModel生命週期比Activity更長。這樣 ViewModel 中存放的業務數據就能夠在Activity銷燬重建時被複用。

數據綁定

MVVM中Activity 屬於V層,佈局構建以及數據綁定都在這層完成:

class MvvmActivity : AppCompatActivity() {
 private var rvNews: RecyclerView? = null  private var newsAdapter = NewsAdapter()   // 構建佈局  private val rootView by lazy {  ConstraintLayout {  TextView {  layout_id = "tvTitle"  layout_width = wrap_content  layout_height = wrap_content  textSize = 25f  padding_start = 20  padding_end = 20  center_horizontal = true  text = "News"  top_toTopOf = parent_id  }   rvNews = RecyclerView {  layout_id = "rvNews"  layout_width = match_parent  layout_height = wrap_content  top_toBottomOf = "tvTitle"  margin_top = 10  center_horizontal = true  }  }  }   // 構建 ViewModel 實例  private val newsViewModel by lazy {  // 構造 ViewModelProvider 實例, 經過其 get() 得到 ViewModel 實例  ViewModelProvider(this, NewsFactory(applicationContext)).get(NewsViewModel::class.java) }   override fun onCreate(savedInstanceState: Bundle?) {  super.onCreate(savedInstanceState)  setContentView(rootView)  initView()  bindData()  }   // 將數據綁定到視圖  private fun bindData() {  newsViewModel.newsLiveData.observe(this, Observer {  newsAdapter.news = it  rvNews?.adapter = newsAdapter  })  }   private fun initView() {  rvNews?.layoutManager = LinearLayoutManager(this)  } } 複製代碼

其中構建佈局 DSL 的詳細介紹能夠點擊這裏。它省去了原先V層( Activity + xml )中的xml

代碼中的數據綁定是經過觀察ViewModel中的LiveData實現的。這不是數據綁定的徹底體,因此還需手動地觀察observe數據變化(只有當引入data-binding包後,才能把視圖和控件的綁定都靜態化到 xml 中)。但至少它讓ViewModel無需主動推數據了:

在 MVP 模式中,Presenter持有View 層接口並主動向界面數據。

MVVM模式中,ViewModel再也不持有View 層接口,也不主動給界面數據,而是界面被動地觀察數據變化。

這使得ViewModel只需持有數據並根據業務邏輯更新之便可:

// 數據訪問接口在構造函數中注入
class NewsViewModel(var newsRepository: NewsRepository) : ViewModel() {  // 持有業務數據  val newsLiveData by lazy { newsRepository.fetchNewsLiveData() } }  // 定義構造 ViewModel 方法 class NewsFactory(context: Context) : ViewModelProvider.Factory {  // 構造 數據訪問接口實例  private val newsRepository = NewsRepositoryImpl(context)  override fun <T : ViewModel?> create(modelClass: Class<T>): T {  // 將數據接口訪問實例注入 ViewModel   return NewsViewModel(newsRepository) as T  } }  // 而後就能夠在 Activity 中這樣構造 ViewModel 了 class MvvmActivity : AppCompatActivity() {  // 構建 ViewModel 實例  private val newsViewModel by lazy {  ViewModelProvider(this, NewsFactory(applicationContext)).get(NewsViewModel::class.java) } } 複製代碼

ViewModel只關心業務邏輯和數據,不關心獲取數據的細節,因此它們都被數據訪問接口隱藏了。

Demo 業務場景中,ViewModel 只有一行代碼,那它還有存在的價值嗎?

有!即便在業務邏輯如此簡單的場景下仍是有!由於ViewModel生命週期比 Activity 長,其持有的數據能夠在 Activity 銷燬重建時複用。

真實項目中的業務邏輯複雜度遠高於 Demo,應該將業務邏輯的細節隱藏在ViewModel中,讓界面類無感知。好比 「將服務器返回的時間戳轉化成年月日」 就應該寫在ViewModel中。

業務數據訪問接口

// 業務數據訪問接口
interface NewsRepository {  // 拉取新聞並以 LiveData 方式返回  fun fetchNewsLiveData():LiveData<List<News>?> }  // 實現訪問網絡和數據庫的細節 class NewsRepositoryImpl(context: Context) : NewsRepository {  // 使用 Retrofit 構建請求訪問網絡  private val retrofit = Retrofit.Builder()  .baseUrl("https://api.apiopen.top")  .addConverterFactory(MoshiConverterFactory.create())  // 將返回數據組織成 LiveData  .addCallAdapterFactory(LiveDataCallAdapterFactory())  .client(OkHttpClient.Builder().build())  .build()   private val newsApi = retrofit.create(NewsApi::class.java)   private var executor = Executors.newSingleThreadExecutor()  // 使用 room 訪問數據庫  private var newsDatabase = NewsDatabase.getInstance(context)  private var newsDao = newsDatabase.newsDao()   private var newsLiveData = MediatorLiveData<List<News>>()   override fun fetchNewsLiveData(): LiveData<List<News>?> {  // 從數據庫獲取新聞  val localNews = newsDao.queryNews()  // 從網絡獲取新聞  val remoteNews = newsApi.fetchNewsLiveData(  mapOf("page" to "1", "count" to "4")  ).let {  Transformations.map(it) { response: ApiResponse<NewsBean>? ->  when (response) {  is ApiSuccessResponse -> {  val news = response.body.result  news?.let {  // 將網絡新聞入庫  executor.submit { newsDao.insertAll(it) }  }  news  }  else -> null  }  }  }  // 將數據庫和網絡響應的 LiveData 合併  newsLiveData.addSource(localNews) {  newsLiveData.value = it  }   newsLiveData.addSource(remoteNews) {  newsLiveData.value = it  }   return newsLiveData  } } 複製代碼

這就是MVVM中的M,它定義了如何獲取數據的細節

Demo 中 數據庫和網絡都返回 LiveData 形式的數據,這樣合併兩個數據源只須要一個MediatorLiveData。因此使用了 Room 來訪問數據庫。而且定義了LiveDataCallAdapterFactory用於將 Retrofit 返回結果也轉化成 LiveData。(其源碼能夠在這裏找到)

這裏也存在耦合:Repository須要瞭解 Retrofit 和 Room 的使用細節。

當訪問數據庫和網絡的細節愈來愈複雜,甚至又加入內存緩存時,再增長一層抽象,分別把訪問內存、數據庫、和網絡的細節都隱藏起來,也是常見的作法。這樣Repository中的邏輯就變成: 「運用什麼策略將內存、數據庫和網絡的數據進行組合並返回給業務層」。

Clean Architecture

經屢次重構,代碼結構不斷衍化,最終引入了ViewModelRepository。層次變多了,表面上看是愈來愈複雜了,但其實理解成本愈來愈低。由於 全部複雜的細節並非在同一層次被展開。

最後用 Clean architecture 再審視一下這套架構:

Entities

它是業務實體對象,對於 Demo 來講 Entities 就是新聞實體類News

Use Cases

它是業務邏輯,Entities 是名詞,Use Cases 就是用它造句。對於 Demo 來講 Use Cases 就是 「展現新聞列表」 在 Clean Architecture 中每個業務邏輯都會被抽象成一個 UseCase 類,它被Presenters持有,詳情能夠去這裏瞭解

Repository

它是業務數據訪問接口,抽象地描述獲取和存儲 Entities。和 Demo 中的 Repository 如出一轍,但在 Clean Architecture 中,它由 UseCase 持有。

Presenters

它和MVP模型中 Presenter 幾乎同樣,由它觸發業務邏輯,並把數據傳遞給界面。惟一的不一樣是,它持有 UseCase。

DB & API

它是抽象業務數據訪問接口的實現,和 Demo 中的NewsRepositoryImpl如出一轍。

UI

它是構建佈局的細節,就像 Demo 中的 Activity。

Device

它是和設備相關的細節,DB 和 UI 的實現細節也和設備有關,這裏的 Device是指除了數據和界面以外的和設備相關的細節,好比如何在通知欄展現通知。

依賴方向

洋蔥圈的內三層都是抽象,而只有最外層才包含實現細節(和 Android 平臺相關的實現細節。好比訪問數據庫的細節、繪製界面的細節、通知欄提醒消息的細節、播放音頻的細節)

洋蔥圈向內的箭頭意思是:外層知道相鄰內層的存在,而內層不知道外層的存在。即外層依賴內層,內層不依賴外層。也就說應該儘量把業務邏輯抽象地實現,業務邏輯只須要關心作什麼,而不應關心怎麼作。這樣的代碼對擴展友好,當實現細節變化時,業務邏輯不須要變。

本文使用 mdnice 排版

相關文章
相關標籤/搜索