本文描述设计模式在Cocoa框架中的主要实现方式,特别是模型-视图-控制器和对象建模模式,主要目的是使您对Cocoa的设计模式有更好的认识,鼓励您在自己的软件工程中利用这些模式。
模型-视图-控制器模式(MVC)是一个相当老的设计模式,它的一些变体至少在Smarttalk的早期就出现了。它是一种高级别的模式,关注的是应用程序的全局架构,并根据各种对象在程序中发挥的作用对其进行分类。它也是个复合的模式,因为它是由几个更加基本的模式组成的。
面向对象的程序在设计上采用MVC模式会带来几个方面的好处。这种程序中的很多对象可能更具重用性,它们的接口也可能定义得更加良好。程序从总体上更加适应需求的改变—换句话说,它们比不基于MVC的程序更加容易扩展。而且,Cocoa中的很多技术和架构—比如绑定技术、文档架构、和脚本技术—都基于MVC,而且要求您的定制对象充当MVC定义的某种角色。
MVC对象的作用和关系
MVC设计模式考虑三种对象:模型对象、视图对象、和控制器对象。模式定义了这三种对象在应用程序中充当的角色,以及它们的通讯路径。在设计应用程序时,一个主要的步骤就是进行这三种对象的选择-或者说为这三种对象创建定制类。三种对象中的每一种都和其它两种按抽象的边界区分,并和其它两种对象进行跨边界的通讯。
模型对象负责包装数据和基本行为
模型对象代表特别的知识和专业技能,它们负责保有应用程序的数据和定义操作数据的逻辑。一个定义良好的MVC应用程序会将所有重要的数据封装在模型对象中。任何代表应用程序留存状态的数据(无论该状态存储在文件中,还是存储在数据库中),一旦载入应用程序,就应该驻留在模型对象中。因为它们代表与特定问题域有关的知识和专业技能,所以有可能被重用。
在理想情况下,模型对象不和负责表示与编辑模型数据的用户界面建立显式的连接。举例来说,如果有个代表一个人的模型对象(假定您在编写一个地址本),您可能希望存储这个人的生日,则将生日存储在您的Person模型对象是比较好的做法。但是,日期格式字符串或其它有关日期如何表示的信息可能存储在别的地方比较好。
在实践上,这种分隔并不总是***的,这里有一定的灵活空间。但一般来说,模型对象不应该关心界面和表示的问题。一个具有合理例外的例子是描画程序,它的模型对象代表要显示的图形。图形对象知道如何描画自身是合理的,因为它们存在的主要原因就是为了定义视觉上的信息。但是即使在这种情况下,图形对象也不应该完全依赖于特定的视图,它们不应该负责描画的具体位置,而应该由希望表示这些图形对象的视图对象发出描画的请求。
进一步阅读:模型对象实现指南文档种讨论模型对象的正确设计和实现。
视图对象负责向用户表示信息
视图对象知道如何显示应用程序的模型数据,而且可能允许用户对其进行编辑。视图对象不应该负责存储它所显示的数据(这当然不是说视图永远不存储它所显示的数据。由于性能上的原因,视图可能对数据进行缓存,或使用类似的技巧)。一个视图对象可能负责显示模型对象的一部分或全部,甚至是很多不同的模型对象。视图对象可能有很多变化。
视图对象应该尽可能可重用和可配置,它们可以在不同的应用程序中提供一致的显示。在Cocoa中,Application Kit定义了大量的视图对象,其中很多对象都出现在Interface Builder的选盘上。您可以通过重用Application Kit的视图对象,比如NSButton对象,来保证应用程序中的按键和其它Cocoa应用程序的按键行为是一样的,从而保证不同的应用程序在外观和行为上具有高度的一致性。
视图必须正确地显示模型,因此需要知道模型发生的改变。由于模型对象不应该依赖于特定的视图对象,所以需要有一个一般性的方式来指示模型对象发生了变化。
控制器对象连接模型和视图
控制器对象是应用程序的视图对象和模型对象之间的协调者。通常情况下,它们负责保证视图可以访问其显示的模型,并充当交流的管道,使视图可以了解模型发生的变化。控制器对象也可以为应用程序执行配置和协调的任务,管理其它对象的生命周期。
在一个典型的Cocoa MVC设计中,当用户通过某个视图对象输入一个值或做出一个选择时,该值或选择会传递给控制器对象。控制器对象可能以应用程序特有的方式对用户输入进行解释,然后或者告诉模型对象如何处理这个输入—比如“增加一个新值”或“删除当前记录”,或者使模型对象在其某个属性上反应被改变的值。基于同样的用户输入,一些控制器对象也可以通知相应的视图对象改变其外观或行为的某个部分,比如禁用某个按键。反过来,当一个模型对象发生变化了—比如加入一个新的数据源—模型对象通常将变化通知控制器对象,由控制器对象要求一或多个视图对象进行相应的更新。
控制器对象可能是可重用的,也可能是不可重用的,取决于它们的一般类型。"Cocoa控制器对象的类型"部分描述Cocoa中不同类型的控制器对象。
组合角色
我们可以将多个MVC角色组合起来,使一个对象同时充当多个角色,比如同时充当控制器和视图对象的角色—在这种情况下,该对象被称为视图-控制器。同样地,您也可以有模型-控制器对象。对于某些应用程序,象这样的角色组合是可接收的设计。
模型-控制器是主要关注模型层的控制器。它“拥有”模型,主要责任是管理模型,并和视图对象进行交流。应用到整个模型的动作方法通常在模型-控制器中实现。文档架构为您提供了一些这样的方法;比如说,NSDocument对象(文档架构的核心部分)会自动处理和保存文件相关的动作方法。
视图控制器是主要关注视图层的控制器。它“拥有”界面(视图),主要责任是管理界面,并和模型对象进行交流。和视图显示的数据相关的动作方法通常在视图控制器中实现。NSWindowController对象(也是文档架构的核心部分)就是视图控制器的一个例子。
"MVC应用程序设计指南"中提供一些关于MVC组合角色对象的设计建议。
进一步阅读:基于文档的应用程序概述从另一个角度讨论模型控制器和视图控制器之间的区别。
Cocoa控制器对象的类型
"控制器对象连接模型和视图"部分粗略介绍了控制器对象的抽象框架,但是在实践中的情景要复杂得多。在Cocoa中有两种一般类型的控制器对象:仲裁控制器和协调控制器。每种类型的控制器对象都和一组不同的类相关联,并提供不同的行为。
仲裁控制器通常是从NSController类继承而来的对象。在Cocoa绑定技术中使用了这种对象。它们负责为视图和模型对象之间的数据流提供仲裁或支持。
仲裁控制器通常都是已经准备好了,可以直接从Interface Builder选盘中直接拖出。您可以对这些对象进行配置,以在视图和控制器对象的属性之间、进而在控制器属性和模型对象的具体属性之间建立绑定关系。结果,当用户改变视图对象显示的值时,新的值就会通过仲裁控制器自动传递给模型对象;而当模型的属性值发生变化时,那些变化又会传递给视图对象。NSController抽象类及其具体子类—NSObjectController、NSArrayController、NSUserDefaultsController、和NSTreeController—提供了诸如提交和丢弃改变的能力,还可以管理选择和占位值的特性。
协调控制器通常是一个NSWindowController或NSDocumentController对象,或者是一个NSObject定制子类的实例。它在应用程序中的角色是检查(或者协调)整个或部分应用程序是否正常工作,比如从一个nib文件解档出来的对象是否有效。协调控制器提供如下服务:
响应委托消息和对通告进行观察
响应动作消息
管理自己“拥有”的对象的生命周期(比如在正确的时间释放那些对象)
建立对象间的连接,并执行其它配置任务
NSWindowController和NSDocumentController类是Cocoa为基于文档的应用程序定义的架构的一部分。这些类的实例为上面列出的几种服务提供了缺省的实现,您也可以通过创建它们的子类来实现更为具体的应用程序行为,甚至可以用NSWindowController对象来管理不基于文档架构的应用程序窗口。
协调控制器通常拥有nib文件中的对象,比如File’s Owner对象,它不属于nib文件包含的对象,但负责管理nib文件中的对象。它拥有的对象包括仲裁控制器、协调控制器、和视图对象。如果需要进一步了解File's Owner及类似的协调控制器的更多信息,请参见"MVC是一个复合的设计模式" 部分的内容。
NSObject的定制子类的实例可能完全适合用作协调控制器。这种类型的控制器对象既有仲裁的功能,也有协调的功能。在仲裁行为方面,它们通过象目标-动作、插座变量、委托、和通告机制来实现视图和模型对象之间的数据移动。它们有可能包含很多“胶水”代码,由于那些代码只用于特定的应用程序,所以它们是应用程序中最不可能被重用的对象。
进一步阅读:如果您需要进一步了解控制器对象作为仲裁者的角色,请参见"仲裁者"设计模式的信息;如果需要进一步了解Cocoa绑定技术的信息。
#p#
MVC是一个复合的设计模式
模型-视图-控制器是一个组合了几个更为基本的设计模式的复合设计模式。这些基本的模式一起定义了MVC应用程序中特有的功能分割和通信路径。但是和Cocoa相比,传统意义上的MVC使用了不同的基本模式,主要表现在应用程序的控制器和视图对象的不同作用上。
在原来的(Smalltalk)概念上,MVC是由合成(Composite)、策略(Strategy)、和观察者(Observer)模式组成的。
合成模式:应用程序中的视图对象实际上是一些嵌套视图的集合,这些视图以一种协调过的方式(也就是视图层次结构)在一起工作。这些显示组件包括窗口、复合视图(比如表视图)、以及单独的视图(比如按键)。用户输入和显示可以在复合结构的任意级别上进行。
策略模式:一个控制器对象负责实现一或多个视图对象的策略。视图对象将自己限制在视觉效果的维护上,而将与应用程序具体界面行为有关的全部决策委托给控制器。
观察者模式:模型对象将状态的变化通知应用程序中感兴趣的对象(通常是视图对象)。
这些模式以图4-4所示的方式协同工作:用户操作视图层次中某个级别的视图,结果产生一个事件。控制器对象接收到这个事件,并根据应用程序的具体逻辑对其进行解释-也就是说,它应用了某种策略。这个策略可以是请求(通过消息)模型对象改变其状态,也可以是请求视图对象(位于视图结构的某个级别上)改变其行为或外观。反过来,模型对象在状态发生变化时会通知注册为观察者的所有对象,如果观察者是个视图对象,则可能会因此更新外观。
图4-4 传统版本的MVC是一个复合设计模式
Cocoa版本的MVC也是一种复合模式,和传统版本有一些类似之处。事实上,基于图4-4的框图构建一个可以工作的应用程序是完全可能的。通过使用绑定技术,您很容易就可以创建一个Cocoa的MVC程序,让程序中的视图对象直接观察模型对象,以接收状态的改变。然而这个设计有个理论上的问题。视图对象和模型对象应该是程序中***可重用性的对象。视图对象代表操作系统及操作系统支持的应用程序的“观感”;外观和行为的一致性是很重要的,这就要求对象是高度可重用的。顾名思义,模型对象负责对问题域的关联数据进行封装,以及执行相关的操作。从设计的角度上看,***让模型对象和视图对象彼此分离,因为这样可以增加它们的可重用性。
在大多数Cocoa应用程序中,模型对象的状态变化通告是通过控制器对象传递给视图对象的。图4-5显示了这种不同的机制,尽管多用了两个基本设计模式,这种通讯机制显得清晰很多。
图4-5 Cocoa版本的MVC也是一个复合设计模式
在这种复合模式中,控制器对象结合了仲裁者模式和策略模式,对模型和视图对象之间的数据流实施双向协调。模型状态的变化通过应用程序的控制器对象传递给视图对象。此外,视图对象在目标-动作机制上采纳了命令模式。
请注意:目标-动作机制使视图对象可以和用户输入或选择进行通讯,这种机制可以在协调或仲裁控制器中实现。但是,机制的设计在不同类型的控制器中也有所不同。对于协调控制器,您可以在Interface Builder中将视图对象连接到它的目标(即控制器对象)上,并为其指定动作选择器,动作选择器必须遵循特定的签名格式。通过成为窗口和全局应用程序对象的委托,协调控制器也可以进入响应者链。仲裁控制器使用的绑定机制也是将视图对象和目标连接起来,并允许动作方法的签名携带可变数量、任意类型的参数。但是仲裁控制器不在响应者链上。
图4-5描述的是改良后的复合设计模式,对其进行改良既有实践上的原因,也有理论上的原因,特别是在使用仲裁者模式的时候。仲裁控制器是从NSController的具体子类派生而来的,除了实现仲裁者模式之外,这些类还提供很多应用程序应该加以利用的功能,比如选择和占位值的管理。如果您不喜欢使用绑定技术,则您的视图对象可以使用象Cocoa通告中心这样的机制来接收模型对象的通告。但是这要求您创建一个定制的视图子类,以便处理模型对象发出的通告。
在一个设计良好的Cocoa MVC程序中,协调控制器对象常常“拥有”归档到nib文件的仲裁控制器。图4-6显示了这两种控制器类型之间的关系。
图4-6 协调控制器作为nib文件的拥有者
MVC应用程序的设计原则
在设计应用程序的模型-视图-控制器时,可以应用下面这些指导原则:
虽然您可以使用NSObject定制子类的实例来作为仲裁控制器,但是没有理由重新实现一个仲裁控制器。相反,您可以使用已经准备好的、为Cocoa绑定技术设计的NSController对象。也就是说,使用NSObjectController、NSArrayController、NSUserDefaultsController、或者NSTreeController实例-或者使用这些NSController具体子类的定制子类的实例来作为仲裁控制器。
然而如果应用程序很简单,而且您对使用插座变量和目标-动作机制来编写仲裁行为所需要的“胶水代码”感觉更好的话,也可以使用NSObject定制子类的实例来作为仲裁控制器。在NSObject的定制子类中,您也可以以NSController的方式来实现仲裁控制器,使用键-值编码、键-值观察、以及编辑器协议。
虽然您可以把不同的MVC角色合并在一个对象中,但是,在总体上***的策略还是保持角色的分离。这可以增强对象的可重用性以及使用这些对象的程序的可扩展性。如果您要把不同的MVC角色合并到一个类中,则首先为该类选择一个主要的角色,然后(为了便于维护)在相同的实现文件中使用范畴来进行扩展,使其具有其它角色的作用。
设计良好的MVC应用程序的目标之一应该是尽可能多地使用(至少在理论上)可重用的对象。特别重要的是,视图对象和模型对象应该是高度可重用的(当然,那些准备好的仲裁控制器对象也都是可重用的)。应用程序的具体行为通常尽可能多地集中在控制器对象中。
虽然让视图直接观察模型并检测状态的变化是可能的,但是这并不是推荐的做法。视图对象应该总是通过仲裁控制器对象来了解模型对象的变化。这有两层意义:
如果您使用绑定机制来使视图对象直接观察模型对象的属性,那么您就忽视了NSController及其子类为应用程序提供的各种好处:选择和占位符的管理,还有提交和丢弃修改的能力。
如果您不使用绑定机制,则必须从现有的视图类派生出子类,并加入处理模型对象发出的状态变化通告的能力。
努力限制应用程序中类代码的依赖关系。一个类对另一个类的依赖越大,就越不具有重用性。具体的推荐规则和两个类在MVC中的角色有关:
视图对象不应依赖于模型对象(虽然对于某些定制视图来说可能是不可避免的)。
视图类不必然依赖于仲裁控制器类。
模型类不应该依赖于除了其它模型类之外的类。
协调控制器不应该依赖于模型类(和视图相似,虽然对某些定制的控制器类来说,这种依赖关系是必须的。)
仲裁控制器类不应该依赖于视图类或协调控制器类。
协调控制器类依赖于所有MVC类。
如果Cocoa提供的架构已经将MVC角色分配给具体类型的对象,则直接使用该架构。这样做可以使您更为容易地将工程的各个组件集成在一起。文档架构就是这样的一个例子,它包括一个Xcode工程模板,并在模板中将NSDocument对象(基于nib的控制器模型)预先配置为File's Owner。
Cocoa中的模型-视图-控制器
模型-视图-控制器设计模式使很多Cocoa机制和技术的基础。因此,在面向对象的设计中使用MVC的重要性已经超过了如何在自己的应用程序中得到更好的可重用性和可扩展性。如果您的应用程序要使用基于MVC的Cocoa技术,则***它本身也是遵循MVC模式。如果您的应用程序很好地进行MVC的分离,在使用这些技术时就应该会相对简单一些;相反,如果没有好的分离,则需要花费更多的努力。
Cocoa框架中包含下面这些基于模型-视图-控制器的架构、机制、和技术:
文档架构。在这个架构中,一个基于文档的应用程序由一个应用程序级别的控制器对象(NSDocumentController)组成的,每个文档窗口都有一个控制器对象(NSWindowController),每个文档(NSDocument)都有一个结合了控制器和模型角色的对象。
绑定技术。 在之前的讨论中曾经提到过,MVC是Cocoa绑定技术的核心。NSController抽象类的具体子类提供了一些准备好的控制器对象,您可以对它们进行配置,建立视图对象和模型对象属性之前的绑定关系。
应用程序的脚本能力。在设计应用程序并使其可以支持脚本控制的时候,不仅需要遵循MVC设计模式,而且需要正确设计应用程序的模型对象。访问应用程序状态和请求应用程序行为的脚本命令通常应该发送给模型对象或者控制器对象。
Core Data。Core Data框架负责管理模型对象图,以及将模型对象存储到一个持久的仓库(还有从仓库中取出),以确保这些对象的持久性。Core Data和Cocoa的绑定技术紧密结合在一起。MVC和对象建模模式是Core Data架构的基本决定因素。
Undo。在Undo架构中,模型对象又一次发挥中心的作用。模型对象的基元方法(常常是它的存取方法)通常是实现undo和redo操作的地方。某个动作的视图和控制器对象也可能参与这些操作。举例来说,您可能有一个方法负责处理undo和redo菜单项的标题,或者undo一个文本视图中的选择操作。
小结:Cocoa模型 视图 控制器设计模式的内容介绍完了,希望本文对你有所帮助!