web mvc,的mvc_你寫的MVC,真的是MVC嗎?

 2023-11-30 阅读 25 评论 0

摘要:看過上一篇文章《寫過長達800行的類,我才明白了這個道理》的朋友,一定對文中那幾個代碼混亂不堪的例子印象深刻,相信你在工作中也碰到過類似的情況。我們已經知道了,MVC設計模式就是用來解決圖形界面工具的代碼分層問題的,今天這篇文章&#

看過上一篇文章《寫過長達800行的類,我才明白了這個道理》的朋友,一定對文中那幾個代碼混亂不堪的例子印象深刻,相信你在工作中也碰到過類似的情況。

我們已經知道了,MVC設計模式就是用來解決圖形界面工具的代碼分層問題的,今天這篇文章,我們就來詳細聊一聊使用MVC的正確姿勢。

web mvc?MVC設計模式為圖形界面工具的代碼劃分提供了很好的指導,它要求代碼遵循“分而治之、各司其職”的原則,將界面顯示的部分和數據處理的部分分離開。

abcf729cf023673a189374898e446e21.png

MVC設計模式最早于1970年代在Smalltalk-76系統中實現,但是其發揚光大卻是得益于Web應用的興起。

mvc分別用什么實現。在Web應用環境中,一方面,為了保證數據源的安全性及穩定服務的能力,數據源及其相關操作被以數據庫的形式部署到了遠端;另一方面,用戶可以使用任意瀏覽器隨時訪問Web應用,同時應用的業務邏輯也日趨復雜,這就自然而然地要求應用進行分層。

需要指出的是,直到今天,MVC設計模式仍然沒有一個準確的定義。

在最初的定義中,應用代碼被分為了三個部分:

  • Model:模型層,負責對接后端數據源,完成數據的讀寫、處理和暫存

  • View:視圖層,負責提供用戶界面,并對模型層的數據進行顯示和輸出

  • Controller:控制層,負責接受用戶的交互操作,并根據業務邏輯調度模型層和視圖層

啟動應用后,用戶直接向Controller層發出請求,Controller層根據請求調用Model層進行數據處理,并通知View層顯示處理結果,于是View層向Model層請求數據。

這種模式下,最終的調用都流向了Model,因此Controller和View均依賴Model,而Model則可以被獨立復用。

a3aef6e24131c2fbd7123a72526d7847.png

后來,不同的框架和技術對MVC的理解和實現方式出現了差異,從而導致了三層的職責范圍和交互關系發生了變化,形成了各種派生模式。

誕生于IBM的MVP(Model-View-Presenter)模式,將Controller替換成了一個展示器(Presenter)層,Presenter作為View和Model之間的中介,負責將View的數據請求轉發給Model,再將Model中的數據包裝成適合View顯示的形式。

affa2bc45a132e6991414a7852179b6a.png

MVP模式完全解除了View和Model之間的依賴關系,兩者互不知曉對方的存在,因此都可以被獨立使用,相比經典MVC模式來說大大提高了代碼的可復用性,并充分貫徹了關注點分離原則。而Presenter依賴Model和View,為兩者的互通提供支撐,因此當Model或View被替換時Presenter也要跟著替換以實現適配。

MVA(Model-View-Adapter)模式與MVP模式非常類似,其中適配器(Adapter)層的作用與Presenter層完全一樣。

aeca6e73e55c2caf33b1f54f7477d2f5.png

兩者的區別在于,MVP模式中,Presenter是在View中進行實例化的,并以View本身作為實例化參數,View和Presenter是一個緊耦合關系。

而MVA模式下,View和Model實例均被傳入Adapter,雖然Adapter仍然依賴View,但是View并不知道Adapter的存在,并且在MVA模式中,同樣一對Model和View可以用不同的Adapter來適配,從而實現不同的數據對接形式。

誕生于微軟的MVVM(Model-View-ViewModel)模式可以說是MVP模式的一種衍生品。

這一模式包含一個視圖模型(ViewModel)層,它是視圖的數據表示層,但是不具備UI相關的邏輯,所以仍然是一個Model層。

框架底層利用觀察者模式實現ViewModel和View的雙向數據綁定,也就是說ViewModel的數據更新了之后,View會自動顯示新的數據,用戶對View的操作也會直接更新ViewModel中的數據。

f046b60e30b3d49e2b938b6ce9debcdb.png

至此,View本身變得非常輕量,它只需負責顯示特定結構的數據,而數據本身的組織、結構化和更新都由ViewModel來完成。

這樣一來,MVVM既能保證View的輕便和統一,又能適應復雜的數據邏輯變化需求,因此很適合用于Web應用和移動應用的開發,像AngularJS、Vue.JS以及Android和iOS開發框架均提供了MVVM模式。

由于雙向數據綁定的存在,View和ViewModel是緊耦合關系,ViewModel更像是View的數據抽象,它們之間有一套預定義的協作模式,而真正獨立作用的Model層則仍然在后端。

下圖展示了這幾種模式的依賴關系:

fe3b884e75739ac6dd38c7c6763ff46e.png

另外還有一種模式叫HMVC(Hierarchical Model–View–Controller),它主要是用來應對復雜的視圖組合的問題。

上述的幾種設計模式中,View都只是一個單一的視圖,而在實際開發中,我們經常會遇到多個視圖嵌套而成的復雜界面,例如一個視頻網站首頁就至少有導航區、Banner區、廣告區、熱點區等等。

我們通常不會把這么復雜的頁面做成單一的視圖,而是每個區域、每個組件獨立開發,每個組件都有自己的MVC架構,最后在主視圖中組合這些組件,處理交互也是由主Controller按照父子關系結構依次調用下層組件的Controller,從而形成一個樹形的MVC拼裝結構。

這也幾乎是目前所有復雜界面應用都會采用的模式。

267c0c22eb48923a3548e6b85d1fde0a.png

那么,Qt框架實現的又是哪種模式呢?按照官方的說法,Qt并沒有嚴格按照MVC模式來進行分層,而是提供了兩種更為簡便但耦合性更高的模式。

首先是簡單模式,在這一模式下,UI控件同時擔當了View和Controller的職責,我們可以直接往控件里添加、刪除數據或者修改數據,數據被操作時也會發出相應的信號。

Qt為我們提供了一系列這樣的簡單控件,包括常用的QListWidget、QTableWidget、QTreeWidget等都是屬于這種模式,這些控件直接拿來用就可以了,而不需要再去子類化它們。這些控件對應的單元項類分別是QListWidgetItem、QTableWidgetItem以及QTreeWidgetItem,但這些控件不能自定義Model。

b17cd81bc5a8cb1d71d3c6f2ff118ed3.png

另一種模型/視圖模式是由Qt的信號-槽機制自動實現View和Model之間的數據更新機制,從而顯式地省略了Controller層。

這乍一看跟MVVM模式很像,但是在MVVM模式里,UI操作是由View自己管理的,ViewModel只作為View層的數據表示層。而在Qt里,View僅僅只用來顯示來自于Model的數據,至于怎么顯示、以及用戶怎么通過View編輯數據從而更新Model,則被一個叫做委托(Delegate)的層接管。

Delegate有兩個作用,一個是控制數據在View中的顯示樣式,另一個是為用戶編輯View中的數據提供編輯器,并更新Model。

Model

在Qt中,所有的Model都繼承自一個抽象基類QAbstractItemModel,這個基類不能直接被實例化,而是必須子類化并實現其中指定的接口之后才能被使用。

對于列表和表格類型的數據集,Qt提供了兩個更為實用的抽象基類QAbstractListModel和QAbstractTableModel,分別針對列表和表格的特性實現了一些接口,如果需要實現列表或表格類型的模型,應當繼承這兩個基類。

eda4905b72499b0a5ac9b33856844076.png

另外,Qt還提供了一系列針對特定數據源的、已經子類化的、可以直接使用的Model,例如QFileSystemModel、QStringListModel、QSqlQueryModel等。

Qt的Model不但包含了與后端數據源通信的機制,更重要的是它為數據集中的每一項提供了統一的指針,這個指針被稱為數據索引。

Qt中基本的數據集類型只有三種:列表表格,而這三種形式中存儲的數據項最終都可以由兩個參數獲得:索引父項,索引又是由行號和列號表示。

這并不意味著后端數據源里的數據要用數組來存儲,數據源可以是各種各樣的結構,而Model提供給View和Delegate的統一接口只需通過索引和父項即可獲取任意一項數據。

fe9d971e22a1e82fcc8a0cd429ba7a1c.png

Model中還有一個概念被稱為角色(Role),用于以不同的形式提供數據項中存儲的數據,通常來說一個數據項可以存儲多個角色,以便為View提供關于該數據項不同方面的信息,例如顯示的字符串和提示。

96db939c63488f3ab27160a3e29aeb34.png

在從Model獲取數據時可以指定使用哪個角色,同時這也有助于Delegate去為不同的角色設置顯示形式。

View

所有的View也繼承自一個抽象基類QAbstractItemView,Qt同樣提供了幾種常用的View的實現,例如QListView、QTableView、QTreeView等,這些實現既可以直接實例化來使用,也可以子類化后添加更多特性。

f59eb14b748c1b861bb63f5cd5bd16da.png

View實現了通過數據索引從Model中獲取數據的接口,同時提供數據的顯示并支持用戶輸入,對于高級的顯示和編輯設置可以交由Delegate來完成。實際上,Qt為幾種標準View設置了默認的Delegate,也就是以QString的形式顯示,以QLineEdit的形式編輯。

View可以獨立于Model來定義和編寫,但是在使用的時候必須指定Model才能顯示出數據來。不同的View可以使用同一個Model,反之同一個View也可以用來顯示不同Model的數據,因為數據接口都是一致的。

某些類型的View除了顯示數據本身,還會顯示列頭或表頭,這是由QHeaderView實現的,它會調用Model中的headerData()函數來獲取頭信息,并以QLabel的形式顯示。

Delegate?

如果想自定義數據項在View的顯示形式,或是用特定的控件來編輯數據項,以及在編輯數據時提供約束和校驗,這些都是Delegate負責的事情。

QAbstractItemDelegate作為抽象基類,其提供的接口包括用于繪制數據項的接口,以及為數據項提供編輯控件的接口,當然還有將編輯結果發送給Model的接口。

Qt4.4之后的版本提供了兩個更為實用的Delegate實現:QStyledItemDelegate和QItemDelegate,兩者均提供了接口的默認實現。

489685ef6a7a227a582543a556e8f938.png

QStyledItemDelegate會使用應用當前的樣式風格來繪制,并且支持Qt Style Sheet(QSS)樣式文件,因此也是目前推薦的實現自定義Delegate時所使用的基類。

介紹完了Qt的模型/視圖模式中的三個部分,再來看三者之間的交互關系就很清晰了。

5311476a418ff52c95ebe87aee293715.png

那么問題來了,Controller去哪里了呢?Delegate是Controller嗎?或者Controller被合并到了Model里?

這是個有爭議的問題,我在StackOverflow和一篇博客中看到了很有意思的觀點。

這一觀點認為,Qt從未在整個應用層面實現MVC,而它所謂的模型/視圖模式,僅僅只能用在單個可編輯的數據集的顯示和操作上,除此之外的UI控件均不在這一模式的管轄范圍內,同時這一模式也忽略了業務邏輯的成分。

以往對于MVC的定義中,View指的是應用中所有用于呈現和交互的東西,也就是界面里的一切控件,就像下面這樣:

d1354098c1755b987878e129dc31ed8f.png

View可以有不同的嵌套層級,由此組成復雜的界面,這跟HMVC的理念是一樣的。

但是在Qt里,View被定義成單個數據集的呈現者,而Delegate用于控制單條數據的呈現,也就是下面這樣:

2d030cf8246631c3055c20eecead119f.png

上圖這個例子中,Delegate決定了每個Item使用藍底白字、圓角矩形框來顯示,而View則決定了使用兩行五列的表格、以特定的單元格間隔來顯示整個數據集。

因此,對于整個界面來說,凡是沒有使用Qt提供的Model和View的,都不屬于模型/視圖設計模式,這包括所有自行組織并管理的UI控件。

e6cd910412bcf4c270fd6b7ef80acf51.png

由此我們可以得出結論,其實Qt并不是省略了Controller,而是Controller根本不在模型/視圖模式的范疇里,但我們仍然可以自己編寫Controller來支撐應用的業務邏輯。

同理,這一設計模式中的Model也并非真正的Model,而更像是ViewModel,因為這個Model必須繼承自Qt提供的Model基類,并且必須實現指定的函數來為View和Delegate獲取數據提供接口。

至于真正用來操作數據源的Model,可以單獨實現,也可以合并到Qt的Model中,甚至由于沒有顯式存在的Controller,Model也可以承擔一部分跟數據相關的業務邏輯。這些決策都是根據具體情況來定。

好在,由于Qt為所有的View、Model和Delegate都定義了統一的接口,所以在Qt環境下這三者仍然具有良好的可復用性、可遷移性和可替換性。也就是說,同一個Model可以直接被用于不同類型的View,而無需做額外的操作,反之亦然。

現在你已經知道了MVC到底是怎么回事,以及MVC各個變種背后的邏輯。

那么,當我們要用PySide/PyQt來編寫一個圖形界面工具時,什么樣的方案最好呢?

很顯然,這個問題沒有標準答案。

但我可以給你一些建議。

首先,對于單個數據集,可以使用Qt提供的模型/視圖模式,因為它非常便捷,我們只需編寫少量接口即可實現很強大的數據呈現和編輯功能。

但是我在這里強調了這一模式僅用于數據集控件,你可以繼承View基類創建自己的控件,但必須符合Qt所定義的那一套數據集規范。

其次,多個控件之間可以使用HMVC模式進行組合。需要注意的是,由于上一步的模型/視圖模式并未提供Controller層,所以要像HMVC設計模式那樣以父子關系來調用Controller的話只能自己編寫Controller,Controller只需控制各個Model,控件本身的控制仍然交給各自的Delegate。

再者,對于整個應用,以及HMVC中的每一個MVC組,建議使用MVA設計模式,因為這種設計模式耦合性最低。

至于在代碼復用的時候,整個應用的View仍然只包含界面控件本身,其中數據集控件仍然可以替換Delegate和Model。

這是針對應用中包含使用了模型/視圖模式的數據集控件的情況,如果不需要數據集控件,或者自行實現數據集控件,那么可以直接使用HMVA的形式。HMVA是我在這里新發明的詞匯,但相信你已經知道這是什么意思了。

至于QtDesigner,請記住,它只是用來做界面的。

1bdb893413224c9ce3ad7eaff388c2f4.png

雖然它提供了信號-槽編輯器,但是這些邏輯僅僅只是界面邏輯,是與業務無關的,它更像是點擊一個按鈕可以顯示一組控件,再點擊就會隱藏這組控件,或者切換下拉菜單選項就可以更改界面的顏色。

這些邏輯和參數是在界面創建之初就確定下來的,并不會隨著業務需求的變化而變化,所以可以隨著界面一起創建好,而業務邏輯則應當在界面之外來完成

QtDesigner里也有Qt最基本的三個數據集View:QListView、QTableView和QTreeView,但如果我們要繼承它們,或者直接繼承QAbstractItemView來實現自定義控件,那么就需要自己去編寫代碼。

最后我們來看兩個具體的例子。

下面這個例子中,Model類繼承自抽象基類QAbstractTableModel,這是一個表格類型的Model,表頭存放于header_list列表,表行以字典的形式存放于data_list列表。

columnCount()和rowCount()用于獲取列總數和行總數,headerData()用于返回指定列的表頭名,data()調用currentData(),根據給定的索引返回數據。可以看到Role在這里起到了限定數據返回的作用。

03619600871454e0545462849a0241cf.png

View繼承自QTableView,非常輕量,除了初始化函數里設置了Model、表頭、選擇模式之外,就只有一個函數用來根據Model中的值重新設置列寬。

這個例子并沒有用到Delegate,你可以根據文末給出的參考資料自行去嘗試Delegate的實現。

第二個例子沒有用到模型/視圖模式,但是用到了QtDesigner和MVA模式。

首先在QtDesigner中搭建好界面,只需要設置好各個控件的布局和名字就行了,無需創建任何信號-槽連接,然后使用pyside2-uic轉換成py文件。

bd74f8ac5c3973030779b59b8f68e5f8.png

接下來編寫Model,首先是用于應用界面的Model類,它存儲了界面會用到的所有的數據,并提供了相應接口。其次還創建了兩個專門的后端Model類,用于特定數據源相關的操作,并在主Model中實例化并調用這兩個后端Model。

c287c8ef83b62701db8501c461639e45.png

然后我們再來編寫Controller類,按照MVA設計模式,需要將Model和View的實例傳入Controller,并在Controller實現業務邏輯,然后將View中特定的控件信號連接到業務邏輯函數上。這些業務邏輯函數會調用Model來進行數據操作,然后更新View。

067658ddaac13e409501ed137ac6c7f2.png

別忘了UI類現在僅僅只是一個Python Object,我們需要用多繼承機制來實現View,然后將各個實例組合到一起形成程序入口。

1d0f04cbe0971d3eb3910a7ae97dff75.png

可以看到,這里的MainWindow類,也就是View類,僅僅只做了初始化UI的操作,除此之外沒有任何邏輯函數。而應用的入口類則將View和Model實例化之后傳入Controller。

這里多說一句,View實例和Controller實例必須設置為入口類的類變量,由入口類持有其生命周期,否則在初始化函數結束后就會被銷毀掉,這樣要么界面顯示不出來,要么業務邏輯函數執行不了,而Model實例傳入Controller后由Controller持有,因此在入口類中無需保存。

上面這個例子中,數據操作全部在Model層完成,Controller負責了應用業務邏輯和線程時序的調度,但其實無論是Qt的模型/視圖模式,還是MVA模式,都一定程度上忽略了Controller的作用,而只關注界面和數據的分離及對接。

對于這一現象,你有什么見解呢?歡迎留言告訴我。

參考資料:

https://www.jianshu.com/p/ebd2c5914d20

https://doc.qt.io/qtforpython/overviews/model-view-programming.html

https://www.devbean.net/2012/08/qt-study-road-2-catelog/

http://imaginativethinking.ca/heck-qt-quick-model-view-architecture/

https://stackoverflow.com/questions/5543198/why-qt-is-misusing-model-view-terminology

f1f33ed70c503a48ab2abe1914321041.gif

往期精選

寫過長達800行的類,我才明白了這個道理

視效公司如何迎接微服務架構(Part1):單體應用,我受夠了

《頭號玩家》最全視效解析,看ILM、DD時隔多年如何再次攜手

一個極客范兒的斜杠青年

帶給你最有料的原創干貨

長按關注52183e5607a613fcbc6ab5415d5211c8.png3831b4de55a4224f4db4546f6239308c.png

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://hbdhgg.com/2/186721.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 匯編語言學習筆記 Inc. 保留所有权利。

底部版权信息