看过上一篇文章《写过长达800行的类,我才明白了这个道理》的朋友,一定对文中那几个代码混乱不堪的例子印象深刻,相信你在工作中也碰到过类似的情况。

我们已经知道了,MVC设计模式就是用来解决图形界面工具的代码分层问题的,今天这篇文章,我们就来详细聊一聊使用MVC的正确姿势。

MVC设计模式为图形界面工具的代码划分提供了很好的指导,它要求代码遵循“分而治之、各司其职”的原则,将界面显示的部分和数据处理的部分分离开。

MVC设计模式最早于1970年代在Smalltalk-76系统中实现,但是其发扬光大却是得益于Web应用的兴起。

在Web应用环境中,一方面,为了保证数据源的安全性及稳定服务的能力,数据源及其相关操作被以数据库的形式部署到了远端;另一方面,用户可以使用任意浏览器随时访问Web应用,同时应用的业务逻辑也日趋复杂,这就自然而然地要求应用进行分层。

需要指出的是,直到今天,MVC设计模式仍然没有一个准确的定义。

在最初的定义中,应用代码被分为了三个部分:

  • Model:模型层,负责对接后端数据源,完成数据的读写、处理和暂存

  • View:视图层,负责提供用户界面,并对模型层的数据进行显示和输出

  • Controller:控制层,负责接受用户的交互操作,并根据业务逻辑调度模型层和视图层

启动应用后,用户直接向Controller层发出请求,Controller层根据请求调用Model层进行数据处理,并通知View层显示处理结果,于是View层向Model层请求数据。

这种模式下,最终的调用都流向了Model,因此Controller和View均依赖Model,而Model则可以被独立复用。

后来,不同的框架和技术对MVC的理解和实现方式出现了差异,从而导致了三层的职责范围和交互关系发生了变化,形成了各种派生模式。

诞生于IBM的MVP(Model-View-Presenter)模式,将Controller替换成了一个展示器(Presenter)层,Presenter作为View和Model之间的中介,负责将View的数据请求转发给Model,再将Model中的数据包装成适合View显示的形式。

MVP模式完全解除了View和Model之间的依赖关系,两者互不知晓对方的存在,因此都可以被独立使用,相比经典MVC模式来说大大提高了代码的可复用性,并充分贯彻了关注点分离原则。而Presenter依赖Model和View,为两者的互通提供支撑,因此当Model或View被替换时Presenter也要跟着替换以实现适配。

MVA(Model-View-Adapter)模式与MVP模式非常类似,其中适配器(Adapter)层的作用与Presenter层完全一样。

两者的区别在于,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中的数据。

至此,View本身变得非常轻量,它只需负责显示特定结构的数据,而数据本身的组织、结构化和更新都由ViewModel来完成。

这样一来,MVVM既能保证View的轻便和统一,又能适应复杂的数据逻辑变化需求,因此很适合用于Web应用和移动应用的开发,像AngularJS、Vue.JS以及Android和iOS开发框架均提供了MVVM模式。

由于双向数据绑定的存在,View和ViewModel是紧耦合关系,ViewModel更像是View的数据抽象,它们之间有一套预定义的协作模式,而真正独立作用的Model层则仍然在后端。

下图展示了这几种模式的依赖关系:

另外还有一种模式叫HMVC(Hierarchical Model–View–Controller),它主要是用来应对复杂的视图组合的问题。

上述的几种设计模式中,View都只是一个单一的视图,而在实际开发中,我们经常会遇到多个视图嵌套而成的复杂界面,例如一个视频网站首页就至少有导航区、Banner区、广告区、热点区等等。

我们通常不会把这么复杂的页面做成单一的视图,而是每个区域、每个组件独立开发,每个组件都有自己的MVC架构,最后在主视图中组合这些组件,处理交互也是由主Controller按照父子关系结构依次调用下层组件的Controller,从而形成一个树形的MVC拼装结构。

这也几乎是目前所有复杂界面应用都会采用的模式。

那么,Qt框架实现的又是哪种模式呢?按照官方的说法,Qt并没有严格按照MVC模式来进行分层,而是提供了两种更为简便但耦合性更高的模式。

首先是简单模式,在这一模式下,UI控件同时担当了View和Controller的职责,我们可以直接往控件里添加、删除数据或者修改数据,数据被操作时也会发出相应的信号。

Qt为我们提供了一系列这样的简单控件,包括常用的QListWidget、QTableWidget、QTreeWidget等都是属于这种模式,这些控件直接拿来用就可以了,而不需要再去子类化它们。这些控件对应的单元项类分别是QListWidgetItem、QTableWidgetItem以及QTreeWidgetItem,但这些控件不能自定义Model。

另一种模型/视图模式是由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,分别针对列表和表格的特性实现了一些接口,如果需要实现列表或表格类型的模型,应当继承这两个基类。

另外,Qt还提供了一系列针对特定数据源的、已经子类化的、可以直接使用的Model,例如QFileSystemModel、QStringListModel、QSqlQueryModel等。

Qt的Model不但包含了与后端数据源通信的机制,更重要的是它为数据集中的每一项提供了统一的指针,这个指针被称为数据索引。

Qt中基本的数据集类型只有三种:列表表格,而这三种形式中存储的数据项最终都可以由两个参数获得:索引父项,索引又是由行号和列号表示。

这并不意味着后端数据源里的数据要用数组来存储,数据源可以是各种各样的结构,而Model提供给View和Delegate的统一接口只需通过索引和父项即可获取任意一项数据。

Model中还有一个概念被称为角色(Role),用于以不同的形式提供数据项中存储的数据,通常来说一个数据项可以存储多个角色,以便为View提供关于该数据项不同方面的信息,例如显示的字符串和提示。

在从Model获取数据时可以指定使用哪个角色,同时这也有助于Delegate去为不同的角色设置显示形式。

View

所有的View也继承自一个抽象基类QAbstractItemView,Qt同样提供了几种常用的View的实现,例如QListView、QTableView、QTreeView等,这些实现既可以直接实例化来使用,也可以子类化后添加更多特性。

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,两者均提供了接口的默认实现。

QStyledItemDelegate会使用应用当前的样式风格来绘制,并且支持Qt Style Sheet(QSS)样式文件,因此也是目前推荐的实现自定义Delegate时所使用的基类。

介绍完了Qt的模型/视图模式中的三个部分,再来看三者之间的交互关系就很清晰了。

那么问题来了,Controller去哪里了呢?Delegate是Controller吗?或者Controller被合并到了Model里?

这是个有争议的问题,我在StackOverflow和一篇博客中看到了很有意思的观点。

这一观点认为,Qt从未在整个应用层面实现MVC,而它所谓的模型/视图模式,仅仅只能用在单个可编辑的数据集的显示和操作上,除此之外的UI控件均不在这一模式的管辖范围内,同时这一模式也忽略了业务逻辑的成分。

以往对于MVC的定义中,View指的是应用中所有用于呈现和交互的东西,也就是界面里的一切控件,就像下面这样:

View可以有不同的嵌套层级,由此组成复杂的界面,这跟HMVC的理念是一样的。

但是在Qt里,View被定义成单个数据集的呈现者,而Delegate用于控制单条数据的呈现,也就是下面这样:

上图这个例子中,Delegate决定了每个Item使用蓝底白字、圆角矩形框来显示,而View则决定了使用两行五列的表格、以特定的单元格间隔来显示整个数据集。

因此,对于整个界面来说,凡是没有使用Qt提供的Model和View的,都不属于模型/视图设计模式,这包括所有自行组织并管理的UI控件。

由此我们可以得出结论,其实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,请记住,它只是用来做界面的。

虽然它提供了信号-槽编辑器,但是这些逻辑仅仅只是界面逻辑,是与业务无关的,它更像是点击一个按钮可以显示一组控件,再点击就会隐藏这组控件,或者切换下拉菜单选项就可以更改界面的颜色。

这些逻辑和参数是在界面创建之初就确定下来的,并不会随着业务需求的变化而变化,所以可以随着界面一起创建好,而业务逻辑则应当在界面之外来完成

QtDesigner里也有Qt最基本的三个数据集View:QListView、QTableView和QTreeView,但如果我们要继承它们,或者直接继承QAbstractItemView来实现自定义控件,那么就需要自己去编写代码。

最后我们来看两个具体的例子。

下面这个例子中,Model类继承自抽象基类QAbstractTableModel,这是一个表格类型的Model,表头存放于header_list列表,表行以字典的形式存放于data_list列表。

columnCount()和rowCount()用于获取列总数和行总数,headerData()用于返回指定列的表头名,data()调用currentData(),根据给定的索引返回数据。可以看到Role在这里起到了限定数据返回的作用。

View继承自QTableView,非常轻量,除了初始化函数里设置了Model、表头、选择模式之外,就只有一个函数用来根据Model中的值重新设置列宽。

这个例子并没有用到Delegate,你可以根据文末给出的参考资料自行去尝试Delegate的实现。

第二个例子没有用到模型/视图模式,但是用到了QtDesigner和MVA模式。

首先在QtDesigner中搭建好界面,只需要设置好各个控件的布局和名字就行了,无需创建任何信号-槽连接,然后使用pyside2-uic转换成py文件。

接下来编写Model,首先是用于应用界面的Model类,它存储了界面会用到的所有的数据,并提供了相应接口。其次还创建了两个专门的后端Model类,用于特定数据源相关的操作,并在主Model中实例化并调用这两个后端Model。

然后我们再来编写Controller类,按照MVA设计模式,需要将Model和View的实例传入Controller,并在Controller实现业务逻辑,然后将View中特定的控件信号连接到业务逻辑函数上。这些业务逻辑函数会调用Model来进行数据操作,然后更新View。

别忘了UI类现在仅仅只是一个Python Object,我们需要用多继承机制来实现View,然后将各个实例组合到一起形成程序入口。

可以看到,这里的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

往期精选

写过长达800行的类,我才明白了这个道理

视效公司如何迎接微服务架构(Part1):单体应用,我受够了

《头号玩家》最全视效解析,看ILM、DD时隔多年如何再次携手

一个极客范儿的斜杠青年

带给你最有料的原创干货

长按关注

的mvc_你写的MVC,真的是MVC吗?相关推荐

  1. 写自己的ASP.NET MVC框架(上)

     开始 ASP.NET程序的几种开发方式 介绍我的MVC框架 我的MVC框架设计架构 回忆以往AJAX的实现方式 MyMVC中实现AJAX的方式 如何使用MyMVC框架中的AJAX功能 配置MyM ...

  2. 写在《ASP.NET MVC 4 Web 编程》即将出版之际!献给有节操的程序员!

    <Programming ASP.NET MVC 4>中文版即将上市了!非常荣幸我可以再次参与一本不错的技术书籍的翻译工作. 这也是在<WCF技术内幕>与<WCF服务编程 ...

  3. java ssh 和mvc_[转]JAVA三大框架SSH和MVC

    Java-SSH(MVC) JAVA三大框架的各自作用 hibernate是底层基于jdbc的orm(对象关系映射)持久化框架,即:表与类的映射,字段与属性的映射,记录与对象的映射 数据库模型 也就是 ...

  4. 写自己的ASP.NET MVC框架(下)

     开始 MyMVC的特点 介绍示例项目 关于URL路由 配置MyMVC框架 映射处理器(入口) 内部初始化 从URL到Action的映射过程 PageUrl的设计思想 多URL的匹配功能 解决老的 ...

  5. [.net 面向对象程序设计深入](4)MVC 6 —— 谈谈MVC的版本变迁及新版本6.0发展方向...

    [.net 面向对象程序设计深入](4)MVC 6 --谈谈MVC的版本变迁及新版本6.0发展方向 1.关于MVC 在本篇中不再详细介绍MVC的基础概念,这些东西百度要比我写的全面多了,MVC从1.0 ...

  6. java mvc mvvm_从MVC到MVVM(为什么要用vue)

    axios 功能类似于jQuery.ajax. axios.post() axios.get() axios.put() axios.patch() axios.delete() 比jQuery.aj ...

  7. mvc原理和mvc模式的优缺点

    mvc原理和mvc模式的优缺点 一.mvc原理    mvc是一种程序开发设计模式,它实现了显示模块与功能模块的分离.提高了程序的可维护性.可移植性.可扩展性与可重用性,降低了程序的开发难度.它主要分 ...

  8. The prefix “mvc“ for element “mvc:annotation-driven“ is not bound 异常

    The prefix "mvc" for element "mvc:annotation-driven" is not bound 异常 参考文章: (1)Th ...

  9. ASP.NET开源MVC框架Vici MVC(三)HELLO WORD

    ASP.NET开源MVC框架Vici MVC 最大的特点是支持ASP.NET2.0  iis不需要额外的设置 官方实例下载地址http://viciproject.com/wiki/Projects/ ...

  10. mvc中的mvc分别指什么_什么是MVC,它像三明治店吗?

    mvc中的mvc分别指什么 by Adam Wattis 通过亚当·沃蒂斯(Adam Wattis) 什么是MVC,它像三明治店吗? (What is MVC, and how is it like ...

最新文章

  1. redis必杀高级:性能测试
  2. 20145129 《Java程序设计》第6周学习总结
  3. Android中Intent连接不同组件的原理
  4. tomcat的缺少tcnative-1.dll的解决
  5. 初识BGP外部网关协议(一)
  6. Advanced C++ --- const function
  7. 【POJ - 1995】Raising Modulo Numbers(裸的快速幂)
  8. LeetCode 1161. 最大层内元素和(层序遍历)
  9. 基于Android平台的简易人脸检测库
  10. 开发岗位面试你应该知道的回答技巧!
  11. 区块链 什么是DAPP
  12. 漏洞扫描器 - cmd命令行执行
  13. 攻防世界hello _pwn总结
  14. 一、Photoshop新版本(2019以后)常用快捷键总结、归纳
  15. FPGA 之 SOPC 系列(五)Nios II 软件使用与程序开发 I
  16. 2010年中国十大最赚钱职业
  17. 活用async/await,实现一些让Vue更好用的装饰器
  18. 计算机网络组件连接方式有,一种计算机网络信号连接装置的制作方法
  19. 充电速度公式_充电电池充电时间计算方法
  20. 【机器学习】模型评估与选择(实战)

热门文章

  1. Mybatis(动态SQL大全)
  2. colormap保存 matlab_matlab中自定义colormap的保存与调用
  3. python3.7降级3.6_电脑已有Python 3.7 怎么降到Python3.6
  4. C#内存共享通讯示例
  5. 【Python 10】汇率兑换3.0(while循环)
  6. html meta标签
  7. loadRunner函数之lr_set_debug_message
  8. C#实现万年历(农历、节气、节日、星座、星宿、属相、生肖、闰年月、时辰)
  9. 再发Wallop和GMail邀请各4个!
  10. python合并两个数据框_python-3.x - 如何使用匹配索引合并两个数据框? - SO中文参考 - www.soinside.com...