iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 19
1
Software Development

什麼?又是/不只是 Design Patterns!?系列 第 27

[Architectural Pattern] MVP pattern for Android

前言:Design Patterns 在上一篇文章告一個段落了,本系列文章從今天開始會介紹五個常見的 Architectural Pattern,依序為 MVP, MVC MVVM, SOA 與 Microservices。

"MVP、MVP、MVP" 在一陣歡呼聲中出場的,是 architectural pattern 三兄弟中的二哥: MVP pattern。

為何我們會把 MVP 放在其他兩位常拿來比較的兄弟 (MVC、MVVM) 之前介紹呢? 除了他是 MVP 之外,他也能提供最清楚的介面,讓我們只需要透過少量的閱讀,就能馬上理解程式所提供的情境及功能。

先讓我們來看看以下的介面定義,是否能夠大概猜到 View 和 Presenter 之間如何串起使用情境呢:

interface ArticleContract {
    interface ArticlePresenter {
        fun onAddArticle()
        fun onArticleSelected(article: Article)
        fun onBack()
    }

    interface ArticleView {
        fun showArticles(articles: List<Article>)
        fun showArticleContent(article: Article)
        fun hideArticleContent()
    }
}

首先我們看到 View,由 View 本身去想像,應該就是需要在畫面上畫出什麼吧!
而 View 有著三個方法,showArticles 需要取得一串文章、showArticleContent 則是...這應該不需要過度解釋。

另一個 Presenter 介面,雖然這個字好像較為抽象,但在閱讀過其提供的三個方法後,大致能夠想像他提供了接收使用者事件的功能吧。

如果介面能夠輕易地被看懂,那代表這個 MVP 設計得算是成功。
清楚的透過介面來定義表達使用情境,並定義情境中 View 層以及 Presenter 層的交互作用,正是 MVP 最大的優點。

在省去一切實作細節的情況下,透過閱讀介面定義,便能夠讓程式在後續維護、增加或修改使用情境、甚至是抽換特定層實作...等種種情況下帶來好處。

MVP 的運作模式

MVP 架構著重在於程式的不同階層間透過合約以及介面認識其它階層開放讓外界知道的方法,而將實作細節封裝於元件內部,用以達到各層級可獨立於彼此實作的效果。MVP 三層各自的職責如下:

  • Model: 定義系統中的基礎物件以及物件本身的行為,用以表示商業邏輯。(MVC, MVP, MVVM 架構經常與 Repository pattern 合用,將所有 Model 的存取方式實作細節也隱藏在此層中)
  • View: 使用者介面層,有著兩項主要職責:透過介面從 Presenter 取得 Model 呈現在畫面上;以及取得使用者輸入並透過介面通知 Presenter。
  • Presenter: 介於 Model 以及 View 之間的階層。作為一位中介者,Presenter 能夠從 Model 取得資料,決定資料的使用方式並交由 View 層進行呈現。另一方面提供介面取得使用者輸入,並將輸入轉為 Model 層所需的資料進行更新。

MVP

View 和 Presenter 共同為畫面呈現負責,兩者各自透過合約認識對方,在需要時直接透過介面呼叫彼此。
而 Presenter 則可以取得 model 的實體,對其進行更新並將結果轉交給 View 進行呈現。

圖上的數字 1~4 代表一個由使用者所觸發的情境的處理順序。情境由 View 接收使用者輸入開始,透過介面通知 Presenter,故 Presenter 端的介面使用情境的描述。
Presenter 在更新 Model 並取得新的資料後主動通知 View 進行呈現,故 View 層的介面通常為呈現資料或改變呈現狀態。

MVP 的優勢及缺陷

  • 優勢

    1. 在開發早期先完成介面,讓團隊成員快速分工
    2. 透過限制不同層之間的溝通來減少不預期的依賴性
    3. 各層可獨立進行測試,並將其它層的元件抽換 (mock 或是 dummy implementation)
    4. 各層的實作可獨立抽換
  • 缺陷

    1. 合約及介面檔案造成程式碼追蹤上的累贅
    2. 持續擴充的使用情境:當情境過於複雜時,Presenter 可能變為 God Class,若要將其拆解成多個 Presenters 則可能遇到多個 Presenter 使用到同一個 Model 時複雜的溝通及更新問題。由於在 MVP 中 Presenter 定位為主動取用 Model 的角色,一個 Presenter 對 Model 的改動所造成的更新需求可能不會被其它 Presenter 觀察到。

Android 範例

這個 Github repository 中實作了 ArticleContract 的範例程式的 MVP, MVC, MVVM 版本,包含以下情境:

  1. 呈現文章列表
  2. 當使用者點擊新增文章按鍵時,增加一篇文章並隨機填入內容
  3. 當使用者點擊一篇文章時,呈現文章內容,並顯示返回按鍵
  4. 當使用者點擊返回按鍵時,文章內容以及返回按鍵消失,呈現文章列表

下面是 Presenter 的實作範例:

class MVPPresenter(private val repository: RunTimeRepository,
                   private val view: ArticleContract.ArticleView): ArticleContract.ArticlePresenter {

   init {
       // 情境1: 初始化,取得初始文章列表並呼叫 View 呈現
       repository.mergeDefaultArticles()
       view.showArticles(repository.getArticles())
   }

   override fun onAddArticle() {
       // 情境2: 創造一篇隨機內容的文章並加入文章列表
       val article = ArticleGenerator.randomArticle()
       repository.addArticle(article)
       view.showArticles(repository.getArticles())
   }

   override fun onArticleSelected(article: Article) {
       // 情境3: 呈現特定文章的內容
       view.showArticleContent(article)
   }

   override fun onBack() {
       // 情境4: 透過隱藏文章列表以返回呈現文章畫面
       view.hideArticleContent()
   }
}

可以注意到 Presenter 並沒有任何依賴於任何 Android 的函式庫,因為我們將所有與 Android 平台相關的內容都封裝在 View 層中實現。

而不管是合約還是 Presenter 當中都沒有提及任何情境中提到的 "返回按鍵" 細節,所以此按鍵的實作同樣完全封裝在 View 當中。

View 的實體則是由 Android 系統提供的 Activity 進行實作,同時兼具系統起始點以及 View 的責任。

class MVPArticleViewActivity : AppCompatActivity(), ArticleContract.ArticleView, ArticleAdapter.ItemClickListener {
   // ... 省略部分實作細節

   private fun subscribeUseCases(presenter: MVPPresenter2) {
       // 情境2
       RxView.clicks(add_article_btn)
           .subscribe {
               presenter.onAddArticle()
           }.addTo(disposableBag)

       // 情境4
       RxView.clicks(back_btn)
           .subscribe {
               presenter.onBack()
           }.addTo(disposableBag)
   }

   override fun onItemClick(view: View, article: Article) {
       // 情境3
       presenter.onArticleSelected(article)
   }

   // 情境1
   override fun showArticles(articles: List<Article>) {
       adapter.setData(articles)
       adapter.notifyDataSetChanged()
   }

   override fun showArticleContent(article: Article) {
       // 返回按鍵的實作
       back_btn.visibility = View.VISIBLE
       articleView.showArticle(article)
   }

   override fun hideArticleContent() {
       back_btn.visibility = View.INVISIBLE
       articleView.showArticle(null)
   }
}

Android 實戰

這裏提供幾個 MVP Pattern 在 Android 實作時容易遇到的問題,這些狀況在理想上都可以有好的解法,但在一線戰鬥現場卻難以達成,大家在應用 MVP 時不仿可以好好思考一下:

  1. Presenter 該不該接觸 lifecycle:
    當我們覺得 Presenter 應該扮演所有處理 Model 相關事情時,可能會讓 Presenter 一併處理 save/restore 或是 onPause 諸如此類的系統生命週期。如此一來 Presenter 容易越加混亂,也需要考量更多與使用者無關的情境。

    有一種做法是將 Activity 同時作為系統起始點看待,讓 Activity 在啟動時得到 Repository 的實體並處理好一切的系統生命週期。但需要注意 Presenter 內部最好不要有散落的 View State,需要將這些 State 一併包在 Model 中處理、或是在宣告 Presenter 時一併作為參數帶入。

  2. 讓 Presenter 接觸 context:
    許多時候我們會想在 Presenter 層利用 context 來拿取各種資源,但這樣做會使得 Presenter 無法從 Android 依賴中抽離,建議將 context 封裝在物件中並提供介面,再由 Activity 注入。

  3. 複雜使用情境下的 Presenter(s):
    複雜情境下容易遇到 "是否維持單一 Presenter、或是將 Presenter 拆解成多個" 的兩難問題。
    維持單一 Presenter 則容易遇到 God Class、分解則會在多個 Presenters 共用同一個 Model 時無法知道何時更新的問題。

    在此情況下,建議換用其它 pattern,或是破壞掉 Presenter-Model 間的關係,讓 Presenter 可以直接觀察 Model 以觸發更新。

作者:Yolung


上一篇
[Design Pattern] Facade 門面模式
下一篇
[Architectural Pattern] MVC pattern for Android
系列文
什麼?又是/不只是 Design Patterns!?32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言