提到MVC时,我们通常会说“MVC设计模式”。的确,MVC是一种古老又经典的设计思想,它将应用程序划分为实体、视图和控制三个逻辑部件,具有逻辑复用、松散耦合等优点。这里从软件架构和软件框架的角度出发,探讨MVC设计思想,并介绍Cocoa框架对MVC的实现。
软件架构
软件架构从根本上说,是一种设计思想。软件架构并不是一个可执行的软件,而是对系统结构组成的规划和职责设定,是系统的蓝图。
一个系统通常可以从逻辑上划分为多个部分:有处理计算的,有处理界面的,有处理数据的,有处理业务规则的,有处理安全的等等。软件架构的意义就在于,它可以将这些可逻辑划分的部分独立出来,用约定的接口和协议将它们有机地结合在一起,形成职责清晰、结构清楚的软件结构。
软件架构是一个逻辑性的框架描述,就好像一个别墅的设计图。它只描述房间的间架结构、楼层之间如何分隔、上下水系统在哪里安装、电路如何布线等。这些仅停留在纸面上,它并没有一个实际的可执行部分。
大部分的软件架构都是由一个设计思想,加上若干设计模式,再规定一系列的接囗规范、传输协议、实现标准等文档构成。
例如:J2EE规范描述了一系列逻辑部件,如Session Bean、Entity Bean、Message Driven Bean、JAAS、JDBC等,描述了这些部件的职责和它们的规范,约定了这些部件之间交互的接口和协议、标准,如SOAP、RMI、WebService等。并规划出一个蓝图,描述如何利用这些逻辑部件来实现一个系统。J2EE并不是一个可执行的软件,而是一个软件架构。
再例如:MVC将应用程序划分为实体、视图和控制三个逻辑部件。规定了这三个逻辑部件的职责和交互规范(稍后详述)。因此,MVC是一个软件架构。
有了软件架构,各个厂商就可以根据软件架构开发出若干可执行的半成品,例如某设计模式的实现框架、接口的实现框架、传输协议的开发包等,这些半成品就是软件框架。开发者可以利用软件框架开发出符合某个架构的应用程序。例如,基于J2EE的软件架构,各个厂商开发出各自的产品,包括开发工具和应用容器,开发者利用这些工具和容器就能方便地开发出符合J2EE规范的应用程序,这些工具和容器就是软件框架。
软件框架
软件框架是软件架构的一种实现,是一个半成品。它通常针对一个软件架构当中某一个特定的问题提供解决方案和辅助工具。因此,如果说架构是一个逻辑的构成,框架则是一个可用的半成品,是可执行的。
例如,MVC是一种软件架构,而Struts、JSF、WEBWork等开源项目则分别以自己的方式实现了这一架构,提供了一个半成品,帮助开发人员迅速地开发一个符合MVC架构的应用程序。而Cocoa框架也是对MVC框架的实现,而且对MVC的设计思想发挥地淋漓尽致。
接下来就结合Cocoa框架,介绍MVC设计思想。
MVC设计思想
MVC中各元素的职责
MVC模式把代码划分为3个不同类型:
- 模型(Model):Model描述了“应用程序是什么”。其用于封装和保存应用程序的数据,同时定义操控和处理该数据的逻辑和运算。而且,Model通常是可以复用的。
- 一个良好的MVC应用程序应该将所有重要的数据都封装到Model中,而应用程序在将持久化的数据(文件、数据库)加载到内存中时,也应该保存在Model中。因为Model本身就代表着业务的特定数据对象。如:日程对象、朋友信息对象等。
- 虽然View是用于展示Model中的数据,但是,MVC设计思想中,Model一般独立于View。例如,对于朋友信息,我们需要存储“生日”,而Model通常只需要存储一个日期字符串,至于“生日”在View中以什么样的格式展示给用户,这就不是Model要考虑的问题。在Cocoa框架中,Model通常都是实体类。
- 视图(View):View是展现给用户的界面,这个不用多说。View通常包括窗口、按钮等控件。其主要目的是显示来自Model的数据,并让用户可以进行交互。
- View中的数据可以来自一个Model的一部分,也可以来自一个完整的Model,甚至来自多个Model。在Cocoa框架中,View也是可复用的。
- View需要保证能够正确展示Model中的数据,因此,就需要知道Model中数据的变化。但是,Model一般不绑定到特定视图,因此,就需要其它方法(后面提到Cocoa框架中是怎么实现的)
- 控制器(Controller):Controller充当View和Model的媒介,将模型和视图绑定在一起,包括处理用户点击、输入等,以此修改Model。反过来,View需要知道Model中数据的变化,也是通过Controller来完成。除此之外,Controller还可以为应用程序协调任务,管理其它对象的生命周期。
Cocoa设计者采用MVC作为设计指导原则,Cocoa框架是始终忠诚于MVC的。在iOS端的Cocoa Touch框架中,View通常都是一些很通用的UI元素,像是界面按钮、xib文件等;而Controller则是明确地指向特定UI元素,指明其如何工作,像是UIViewController、UITableViewController等;而Model则是一些实体类等,是完全独立于UI。
在典型的Cocoa MVC设计中,用户通过View进行了一个输入,View就会通知Controller,Controller会理解用户在View上的操作,同时告诉Model如何去处理这个输入,例如,添加一条数据,修改某个字段等。Model据此修改数据。基于用户的输入,Controller也会告诉View去改变它的状态,如“使一个Button变为不可用状态”等。相反的,如果Model有了变化(如接入了新的数据源),Model也会告诉Controller去更新1个或多个View,以便显示Model中数据的变化。
MVC中各元素的通信
下面结合斯坦福大学iOS课程中的内容,说明Cocoa框架中,Model、View、Controller是如何通信的。具体如下图:
这幅图以交通规则的方式来描述Model、View和Controller之间的通信。Model和View之间是双黄线,这代表这两者之间是不能进行通信的。从Controller到View和Model都是白色虚线,这代表Controller向View和Model的通信是完全没问题的,而反过来是白色实线,这代表Model和View并不能毫无限制地与Controller通信,而是需要通过其它方式,例如View到Controller的delegate,Model到Controller的广播等。
下面具体说明Cocoa MVC中的实现:
Controller → Model:Controller需要知道Model的一切,并且有能力与Model完全通信,按照任何方式使用其公共API。因为Controller的一个很重要的工作就是将Model呈现给用户、使用视图作为其仆从。
Controller → View:Controller和View的通信也是完全不受限制,因为Controller可以随时吩咐View构建用户界面,view中的元素会在controller中创建一个outlet,controller通过outlet可以完全掌控view。包括view有哪些更新,需要显示写什么等等。(Outlet:当我们有一个属性,是从Controller指向View,这个属性就叫Outlet。这个属性通常是weak引用的,因为view本身strong保持这个属性,而一旦这个元素被移出view,该元素在堆中的空间就会被释放,同时指针置为nil。这也正好是我们想要的,因为元素被移除view,也就不会再更新或者获取它了。)
View → Controller:View对象是通用的,所以不能知道太多Controller的东西。所以View到Controller是以盲的方式通信的,即不知道Controller中的类,而是通过一种结构化方式(两端都同意这样通信),具体包括以下3种:
- 目标动作(Target Action):Controller在自身设置一个目标(Target),他会提供一个动作(Action)给View,当view有此动作时,就会触发相应的方法。即对于View,当View需要做什么时,如view是个按钮,当有人点击按钮时;或View是个滑动条,当有人动了这个滑动条时,就把这个Action发送给Controller。这样View不知道到底是纸牌游戏控制器还是空间游戏控制器,或者其他控制器,只需要发现有人动了,发送action给target(Controller)即可。View想告诉控制器发生了什么事,就会向控制器的Target发射一个action,就像射箭一样。控制器收到后,就会执行相应的方法。注意,Action是在Controller定义的,其代表Controller可以响应的动作,如按钮按下,滑动条滚动等。Action与outlet正好相反。
- 委托(delegate):有时动作比较复杂,一个Action无法描述清楚,这里设置三个Action:will、should、did描述。然而,View不处理这些逻辑。因此,就需要用协议对这些Action进行封装,凡实现此协议的Controller,就知道有哪些Action,需要实现哪些Action。此时,这个Controller就是委托。比如滚动视图,滚动视图想要Controller知道用户是“已经”滚动过了;或者用户刚按到这里想要滚动,滚动视图要告诉Controller“将要”滚动,或者用户刚按到这里,滚动视图想知道是否可以滚动“should”?所有这些问题,滚动视图没有足够的逻辑去知道答案,所以滚动视图可以把这些问题的权力委托给其他对象,滚动视图甚至不知道这些对象属于哪个类,但是知道这些对象可以处理这些逻辑。在委托协议中提供这些方法,视图控制器只需要实现对应的委托方法,就能响应相应的动作。
- 数据源(Datasource):有时View不关心will、should、did,而是想知道数量,像是音乐播放器,视图不应该“拥有”它们所显示的数据,即数据不是视图类内部的属性,而是Model的。这时View想知道歌曲的Count,就使用Datasource,Datasource不关心will、should、did,它会要求计数。这时Controller去Model中查看,有1000首歌,返回给View,View就会做相应的处理。当要求指到260首歌曲时(滚动条),这时View会向Controller发送消息:给我第260首歌曲,还有后面10个(UITableview的单元格重用),Controller就会回到Model请求更多数据,然后返回给View。这就是View通过Controller和Model通信的过程。Datasource也是一种特殊的委托。所以在iOS中有一些类有数据源,同时通常也还有委托,如UITableview等。
Model → Controller:通知(notification)和键值观察(key value observing KVO),就像Model进行电台广播(Radio Station broadcast)。Model对UI一无所知,但是有时数据源会发生改变,如网络数据库中某个网络位置上的数据发生了变化。控制器需要知道这些信息,就使用类似广播的机制,即通知和键值观察。Controller会接收这样的消息,如果发生变化,Controller会直接与Model通信。
一个app可能有多个View,一个View中也会有多个不同区域,这样就需要合并多个MVC,一个MVC可以将其他MVC作为自己视图的一部分。所以MVC也是一个层次的关系,一个MVC可以作为另一个较大的MVC的仆从,并且可以一直往下串联,形成更复杂的应用。如日历App,先有一整年的View,点击某月进入月View,点击某日进入日View,某日可能有日程信息,点击进入日程View。各个View有不同的信息,都有自己的MVC,其中后3个MVC是顶层MVC的仆从。
参考及推荐阅读:
《Thinking in UML》 谭云杰
被误解的MVC和被神化的MVVM · 唐巧