iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 19
1

今天為各位介紹的主題是: "Model-View-Controller" MVC architecture pattern,以及它在 Android 平台上的實作及應用。

MVC 是一個很有趣的 pattern,有趣的點在於每個人對他的理解都有點類似,但卻又有點不同。所以這次特別找尋了許多資料,想從 MVC pattern 的源頭以及發展來幫助說明為何會有這樣的情況,嘗試歸納其各種流派的共同與不同之處,並提及在 Android 中較流行的實作方式。

MVC 的歷史

從 Wikipedia 上我們可以查到,MVC 最初是由 Trygve Reenskaug 在 1979 年歸納出的 pattern [1,2],當時正值個人電腦發展的早期,圖形介面、鍵盤、滑鼠作為鐵三角 (?) 的使用情境逐漸成型。先讓我們來看看此時 Trygve 對於 MVC 的解釋 (對原文做了部分的化簡及個人解讀,建議閱讀原文):

  • Model: Model 代表了知識,它可以是單一物件或物件的集合。Model 中的節點應表示相同的問題級別,不好的設計會將其與實作細節混在一起。 在 [2] 當中也提及:在一個應用當中的 model 代表了其特殊領域中以軟體模擬或實作的應用核心架構。例如一個加法器的 model 可能為一個單純的 Integer、或是文字編輯的 model 為一個 String。

  • View: View 是一種對於 model 的 (視覺化) 呈現,它在呈現時會強調 model 中特定的屬性並忽視其餘的內容,因此它可扮演在呈現上所用的過濾器。View 會被附加至 model 上,從 model 取得呈現上必要的資訊,或要求 model 更新。而 view 所有的需求都必須反映到 model 中所定義的語法,所以 view 需要認識 model 的實體。

  • Controller: Controller 是使用者與系統之間的橋樑。它透過安排 views 的排列以及呈現對使用者進行輸出。它也能夠透過呈現各式選單或有意義的命令或資訊,來接收使用者的輸入。Controller 取得使用者輸入,將其轉譯為適合的資訊並傳遞下去。

Controller 永遠不應該附加/依附於 (supplement to) view,例如它不會知道如何透過畫出一個箭頭來連結兩個 view。

View 永遠不應該知道使用者的輸入,例如滑鼠或按鍵點擊。從 controller 撰寫方法發送訊息給 view 去重現任何順序的使用者指令是可能的。

嗯...40 年前的文章還真是難以理解,還是用 wiki 上的圖片來說明吧:

Original MVC

圖中顯示了 MVC 三個元件與使用者的關係。Controller 作為取得使用者輸入的介面,將訊號輸入至 Model,而在 Model 中的商業邏輯更新其狀態後,Model 主動通知 View 更新,使用者便透過 View 觀察到更新過後的結果。

所以 Controller 和 View 之間完全不認識彼此,且兩者的實作都依賴於平台。僅有 Model 能夠純粹的獨立於平台,單純的處理商業邏輯並保存資料。

MVC 在當時提供了一種輕便簡潔的方式帶來關注分離的效果,使得逐漸複雜的圖形化使用者介面處理可以和資料流分離,並且提供核心的商業邏輯能夠抽離於平台進行獨立測試的可能性,且在資訊流上能夠從使用者輸入一直到畫面輸出始終保持單向性。

它帶來的總總好處,也幫助開發者能夠方便的協作,只要團隊成員間對於 MVC 的理解一致,便能夠快速的達成分工和整合。

MVC 的概念在日後被大量地被移植到不同的使用情境底下,幫助各語言、平台開發者對架構進行分層;而在此時,MVC 一詞所代表的意義也開始有了不同的解讀。

各種語言/平台/架構使用了部分最初 MVC 的概念,它們對於不同的呈現及控制需求,(可能) 重新定義了 MVC 當中個 component 在不同情境下的職責,最終造成了今日每個人對 MVC 的認知都有些相似、又可能有些不同的狀況。

在不同定義下 (被稱為) MVC 的各種架構

原始的 MVC

MVC 變形範例

可以看到在這種變形底下,Controller 開始出現在 View 以及 Model 的中間,"取得使用者在系統層的原始輸入" 這項責任被轉移到 View;而 Controller 進一步的將原始輸入轉為商業邏輯中定義的合理輸入,用以更新 Model。

MVC 變形範例

可以看到在三個變形的範例中,處理使用情境流程的前兩步驟是相同的,而在 "如何讓 View 取得更新過後的 Model 用以更新畫面" 的方式則大有不同。在第一個變形中,View 透過觀察 Model 來得知更新,而後續兩個變形中,則是由 Controller 通知 View 此一事件。

MVC 變形範例 (此變形又稱 Model2)

這種變形方式稱為 Model2,在 1998 年被提出應用於網路中的 server-client 架構。其中 View 為用戶端 (client) 的網頁呈現,而 Controller 則為伺服器端 (server) 的程序。在 server-client 架構之下,View 難以直接取得 Model,所以出現了此種凡事透過 Controller 的變形。也就是除了來自 View 的使用者輸入外,所有事情都由 Controller 主動完成。

各種變形不一定能夠滿足最初 MVC 的所有設定,但它們在不同語言、平台以及系統架構...等等的環境限制下,發展出了自己對 MVC 的解讀以及應用。
這也是為何我們時常聽到人們對於 MVC 有不同解讀的原因。

Android 實作

程式範例請參考我的 Github repository
當中的 MVC 有著三種實作,以下分別介紹。

  • 實作範例1:
    在 Android 以 open source 之姿問世後,開發者們也開始將自己對於 MVC 的理解整合到 Android 上,但 Android 平台的大鍋炒特性 (程式執行起始點以及 View group 都綁在 Activity 元件上,而且使用者輸入是透過 View 取得),讓開發者們紛紛覺得 "Android 的 Activity 同時實作了 MVC 中的 View 以及 Controller",導致早年的 Android 開發,幾乎只有分成 Activity 以及 Model 兩層。 (Code 很冗長就不貼了)

  • 實作範例2:
    這個實作實現了 Model2 的概念,可以看到在創造文章的情境中,Controller 主動更新 Model,並取得更新結果用以通知 View。

  class MVCController(private val repository: RunTimeRepository,
                    private val view: MVCActivity) {

    init {
        if (repository.getArticles().size == 0) {
            repository.mergeDefaultArticles()
        }
        view.onUpdate(repository.getArticles())
    }

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

    fun selectArticle(article: Article) {
        view.showArticleContent(article)
    }

    fun backFromArticle() {
        view.hideArticleContent()
    }
}
  • 實作範例3:
    這個範例實作了第一種變形:Controller 更新 Model,View 主動觀察 Model 並取得資料用以呈現。
    以下是部分 View 的內容。
class MVCActiveRepositoryActivity : AppCompatActivity(), ArticleAdapter.ItemClickListener {
    // ... 省略部分實作細節

    override fun onCreate(savedInstanceState: Bundle?) {
        // View 訂閱 Model 更新的通知
        repository.addArticleListChangedSubscriber(onArticleListChanged)
        repository.addArticleChangedSubscriber(onSelectedArticleChanged)

        subscribeDataChanged()
        // ...
    }

    private fun subscribeDataChanged() {
        // 情境2 當中 View 更新的方式:View 得知 model 更新後,主動取得資料
        onArticleListChanged
            .subscribe {
                onUpdate(repository.getArticles())
            }.addTo(disposableBag)
    }
}

MVC 的優勢及缺陷

  • 優勢

    1. 提供一種極易理解的職責區分方式,讓開發者能夠輕鬆的對元件進行職責分離。
    2. 許多現成的框架可套用。
    3. 在單一平台/框架下的學習成本平易近人 (?)。
  • 缺陷

    1. MVC 過於古老而泛用,導致定義以及實作方式眾多而彼此之間可能有所矛盾。
      • 在轉移平台/框架/團隊時可能出現不同的解讀而不自覺。
      • 容易因為解讀不同而開戰 (?)。
      • 放棄理解: 所有不在 V 和 M 裡面的東西都塞進 C 就對了。
    2. 仍然有著許多平台依賴性。
    3. Controller 容易成神 (God class)。

結語

MVC 於 40 年前橫空出世,以一己之身開創了大 pattern 時代,啟發了許多開發者,並在各種環境中孕育了無數後進 (?)。
時至今日,雖然我們已經很難對 MVC 進行嚴謹的定義,且在不同環境下討論 MVC 經常帶來各式各樣的爭論以及誤解。
但 MVC 仍舊是一種流行的 pattern,活躍於各個語言及框架中。

相信開發者們在知道 MVC 的歷史後,都能夠掌握其核心精神,並且以開放的心胸認識不同應用情境下的 MVC 實作吧。

參考資料

[1] Trygve Reenskaug, Notes and Historical documents, http://heim.ifi.uio.no/~trygver/1979/mvc-2/1979-12-MVC.pdf, 1977
[2] Krasner, Glenn E.; Pope, Stephen T. (Aug–Sep 1988). "A cookbook for using the model–view controller user interface paradigm in Smalltalk-80", https://www.lri.fr/~mbl/ENS/FONDIHM/2013/papers/Krasner-JOOP88.pdf, 1988
[3] http://heim.ifi.uio.no/~trygver/themes/mvc/mvc-index.html

作者:Yolung


上一篇
[Architectural Pattern] MVP pattern for Android
下一篇
[Architectural Pattern] MVVM pattern for Android
系列文
什麼?又是/不只是 Design Patterns!?32

尚未有邦友留言

立即登入留言