原文地址:https://developer.apple.com/library/ios/documentation/General/Conceptual/CocoaEncyclopedia/Model-View-Controller/Model-View-Controller.html#//apple_ref/doc/uid/TP40010810-CH14
模型视图控制器(MVC)设计模式已经非常老了。它的变化发生在至少从Smalltalk语言的初期。它是一个高层次的模式,它涉及到一个应用程序的全局架构,并根据他们在应用程序中发挥的大致的作用给对象分类。它也是很多基本模式的复合模式。
面向对象程序在它们的设计上使用MVC设计模式在很多方面都很有效益 。在这些程序中的许多对象具有更好的可重用性,接口也更清晰。这个程序能更好的适应需求的变更,换句话说,他们比那些不基于MVC的程序更容易扩展。此外,在Cocoa中很多技术和架构,如绑定,文档体系和脚本,都是基于MVC设计并要求你自定义的对象也扮演MVC其中的一个角色。
MVC对象的角色和关系(Roles and Relationships of MVC Objects)
MVC设计模式有三种类型的对象:模型对象,视图对象和控制器对象。MVC模式定义了这些对象在应用程序中的作用和它们的联系。当设计一个应用程序时,一个主要的步骤是选择或创建属于这三个组之一的对象的自定义类。三种类型的对象中的每一个都是通过抽象的边界与其他类型的对象分离,并在这些边界上与其他类型的对象进行通信的。
模型对象封装数据和基本行为(Model Objects Encapsulate Data and Basic Behaviors)
模型对象表示特殊的知识和专业知识。他们持有应用程序的数据并定义操作该数据的逻辑。一个设计良好的MVC应用程序会将所有重要的数据封装在模型对象中。当应用程序的数据加载到应用程序时,应用程序的任何持久状态数据(无论是否保存在文件或数据库中的持久状态)都应该归属于模型对象。因为他们代表了一个特定的问题域的知识和技术,因此他们往往是可重用的。
理想情况下,一个模型对象和用于显示和编辑的用户界面没有直接的联系。例如,如果你有一个模型对象代表一个人(比如说你写一个地址簿),你可能想存储生日。将它存储在你的Person模型对象中是一个好的主意。然而,存储日期格式化字符串或其他关于如何展示日期的信息可能放在其他地方更好。
在实践中,这种分离并不总是最好的选择,这里有一定的灵活性,但一般来说,一个模型对象不应该牵涉到界面和展示的问题。有一个例子,其中有一点例外是合理,一个绘图应用程序,该应用程序有表示图形显示的模型对象。对于图形对象来说知道如何绘制它们自身是合理的,因为他们的存在的主要原因是定义一个视觉的东西。但是,即使在这种情况下,图形对象不应该依赖于存在于一个特定的视图或任何视图,并且他们不应该负责知道什么时候画自己。他们应该被那些要展示它们的视图来要求画自己。
视图对象给用户展示信息(View Objects Present Information to the User)
一个视图对象知道如何显示来自于应用程序模型的数据,并可能允许用户编辑。视图不应该负责存储它所显示的数据。(这并不意味着视图永远不会存储它的显示数据,当然。一个视图可以缓存数据或做类似的技巧出于性能的考虑)一个视图对象可以负责显示模型对象的一部分,或整个模型对象,甚至许多不同的模型对象。视图有许多不同的品种。
视图对象往往是可重用的和可配置的,它们提供应用程序之间的一致性。在Cocoa,AppKit框架定义了大量的视图对象并在Interface Builder的library提供很多这些对象。通过重用AppKit的视图对象,如NSButton对象,你保证按钮在您的应用程序可以像其他任何Cocoa应用程序按钮一样,确保高水平的跨应用程序的外观和行为的一致性。
视图应该确保它正确地显示模型。因此,它通常需要知道模型的变化。因为模型对象不应该被绑定到特定的视图对象,所以他们需要一个通用的方式来指示他们已经改变了。
控制器对象将模型与视图联系在一起(Controller Objects Tie the Model to the View)
一个控制器对象就像应用程序视图对象和它的模型对象之间的中介。控制器通常是负责确保视图有访问他们需要显示的模型对象,并作为导管,通过它视图可以了解模型的变化。控制器对象也可以执行应用程序的设置和协调任务,并管理其他对象的生命周期。
在一个典型的Cocoa MVC设计,当用户输入一个值,或者通过一个视图对象表示一个选择,输入值或选择值会传送给控制器对象。控制器对象可能会解释用户输入通过某些专用的方式,然后可能会告诉一个模型对象如何处理这些输入,例如,“添加一个新的值”或“删除当前记录”-或它会使模型对象映射一个改变的值在其属性之中。基于这个相同的用户输入,一些控制器对象也可能告诉一个视图对象改变它的外观或行为的一个方面,如告诉一个按钮来禁用自己。相反,当模型对象更改时,说,一个新的数据源被访问啦,模型对象通常将这些变化传递到控制器对象,然后控制器对象请求一个或者多个视图对象来相应的更新自己。
控制对象可以是可重用或不可重用的,这取决于他们的一般类型。Types of Cocoa Controller Objects描述了Cocoa中的不同类型的控制器对象。
结合作用(Combining Roles)
你可以将MVC的不同角色结合成一个对象,例如,使用控制器和视图两种角色,在这种情况下,它会被称为一个视图控制器,同样的,你也可以创建一个模型-控制器对象,对于很多应用程序,像这样的结合作用是可以接受的设计。
模型控制器是一个控制器,它主要关注模型层。它“拥有”这个模型,它的主要职责是管理模型和与视图对象进行通信。适用于模型的操作方法作为一个整体在模型控制器中被实现。文档架构为你提供了一些这些方法;例如,一个NSDocuments对象(这是文档架构的核心部分)自动处理保存文件相关的动作方法。
视图控制器是一个控制器,它主要关注的是视图层。它“拥有”界面(视图);它的主要职责是管理界面和与模型的通信。有关在视图中的数据显示的操作方法通常在视图控制器中实现。NSWindowController的对象(也是文档体系结构的一部分)是一个视图控制器的一个例子。
Design Guidelines for MVC Applications提供了一些有关合并MVC角色对象的设计建议。
进一步阅读:Document-Based Applications Overview从另一个角度讨论了模型控制器和视图控制器之间的区别。
Cocoa控制器对象类型(Types of Cocoa Controller Objects)
Controller Objects Tie the Model to the View勾画了一个控制器对象的抽象轮廓,但在实践中的图片是更复杂的。在Cocoa中有两种一般类型的控制器对象:中介控制器和协调控制器。每一种控制器对象都关系到一组不同的类,并且每个类都提供了一组不同的行为。
中介控制器通常是继承自NSControllerclass的一个对象。中介控制器对象被用于Cocoa绑定技术中。它们促进或调解视图对象和模型对象之间的数据流。
iOS注:AppKit实现了NSController类及其子类。这些类和绑定技术在iOS不可用。
中介控制器是典型的现成的对象,你可以从Interface Builder的 library中拖出。可以配置这些对象,以建立视图对象的属性和控制器对象的属性之间的绑定,然后在这些控制器属性和模型对象的特定属性之间进行绑定。因此,当用户改变显示在视图对象中的值时,新的值将通过中介控制器被自动地传递给模型对象去存储;当模型的属性发生改变时,该变化被传递给视图以显示。NSController抽象类及其具体的子类NSObjectController,NSArrayController,NSUserDefaultsController,和nstreecontroller提供支持功能,如提交和丢弃变化,管理选择器和占位符。
协调控制器通常是一个NSWindowcontroller或NSDocumentcontroller对象(只在AppKit可用),或一个NSObject自定义子类的实例。其在应用中的作用是监督和协调整个或者部分应用程序的功能,如一个对象从一个nib文件解归档。一个协调控制器提供以下服务,如:
响应委托消息和观察通知
响应操作消息
管理所有的对象的生命周期(例如,在适当的时间释放它们)
建立对象之间的连接和执行其他设置任务
NSWindowController和NSDocumentController都是Cocoa架构中基于文档的应用程序的类。这些类的实例为上面列出的几个服务提供默认实现,您可以创建它们的子类来实现更多的特定的行为。你甚至可以使用NSWindowcontroller对象来管理应用程序中的不是基于文件体系结构的windows 。
协调控制器经常拥有的对象在一个nib文件存档。作为文件的所有者,协调控制器处于nib文件中的对象的外部并管理这些对象。这些拥有的对象包括中介控制器以及窗口对象和视图对象。通过MVC as a Compound Design Pattern查看更多关于协调控制器作为文件持有者的信息。
自定义NSObject的子类的实例完全适合作为协调控制器。这些类型的控制器对象结合了中介和协调功能。对于他们的中介行为,他们利用 target-action, outlets, delegation, 和 notifications机制来促进数据在视图对象和模型对象之间的传递。他们往往包含大量的胶水代码,因为该代码是应用程序特定的,因此它们是应用程序中最不可重用的对象。
进一步阅读:更多关于Cocoa绑定技术,看Cocoa Bindings Programming Topics。
MVC是一个复合设计模式(MVC as a Compound Design Pattern)
模型视图控制器是由几个更基本的设计模式组成的一种设计模式。这些基本模式一起定义功能性分离和MVC应用程序特有的通讯的路径。然而,MVC的传统观念提出的一套基本模式不同于那些Cocoa所提出的。这种区别主要在于应用程序的控制器和视图对象的角色。
在原始(Smalltalk)概念,MVC是由复合,策略,和观察者模式组成。
复合-在应用程序中的视图对象实际上是一个嵌套视图的组合,它以协调的方式协同工作(即视图分层)。这些显示组件的范围从一个窗口到复合视图,如表视图,到单个视图,如按钮。用户输入和显示可以发生在复合结构的任何一层。
策略-一个控制器对象实现一个或多个视图对象的策略。视图对象限制它自身用来视觉展示,并且将所有特有的界面行为决议委托给控制器。
观察者-一个模型对象在应用程序中保持对对象的兴趣-通常是视图对象-建议对其状态的改变。
传统方式的复合,策略,和观察者模式工作示意图如图7-1所示: 用户通过操作一个视图(某种程度的复合结构)并产生一个事件。一个控制器对象接收事件并通过专用的方式解释它-它运用一个策略。这个策略可以是请求(通过消息)一个模型对象来改变它的状态或请求一个视图对象(在某种程度上的复合结构)改变其行为或外观。模型对象相应的通知所有已经注册观察者的对象当它们的状态发生变化的时候,如果观察者是一个视图对象,它可以相应地更新其外观。
MVC的Cocoa版本作为一个复合模式和传统版本有很多相似之处,事实上很有可能构建基于图7-1图的应用。通过使用绑定技术,您可以轻松地创建一个Cocoa的MVC应用程序,它的视图直接监听模型对象并通过接收通知来得知状态的变化。然而,这种设计是一个理论问题。视图对象和模型对象应该是应用程序中最可重用的对象。视图对象代表一个操作系统和支持的应用的“外观和感觉”;在外观和行为上的一致性是必不可少的,这就需要高度可重用的对象。根据定义模型对象封装和问题域相关的数据,并在这些数据上执行操作。明智的设计,最好保持模型和视图对象彼此分开,因为这样提高了他们的可重用性。
在大多数Cocoa应用程序中,模型对象的状态变化的通知通过控制器对象来传递给视图对象。图7-2显示这些不同的结构,这看起来似乎更整洁尽管涉及到两个更基本的设计模式。
该复合设计模式中的控制器对象采用了中介模式和策略模式,它在两个方向上协调模型对象和视图对象之间的数据传递。模型状态的变化通过应用程序的控制器对象来传递给视图对象。此外,视图对象通过它们的target-action机制的实现来混合命令模式。
注意:target-action机制,它使视图对象能够在协调和中介控制器对象中实现用户输入和选择的通信。然而,该机制的设计在不同的控制器类型中有所不同。对于协调控制器,您在Interface Builder中将视图对象连接到它的目标(控制器对象),并指定必须符合某个签名的选择器。协调控制器,由于的是窗口和全局应用程序对象的委托,也可以在响应链中。由中介控制器使用的绑定机制也将视图对象连接到目标,并允许具有可变数量的任意类型的参数的动作签名。然而中介控制器不在响应链中。
修订的复合设计模式有很多实际和理论的原因如图7-2所描述,特别是当它涉及到中介设计模式。中介控制器来自NSController具体的子类,这些类,除了实现中介模式,还提供了许多应用程序可以使用的功能,如管理选择器和占位符。如果您选择不使用绑定技术,您的视图对象可以使用诸如Cocoa通知中心这样的机制来接收来自模型对象的通知。但这将要求您创建一个自定义视图子类,以添加监听来自于模型对象发布的通知。
在一个精心设计的Cocoa的MVC应用程序中,协调控制器对象通常拥有中介控制器,这些中介控制器由nib文件存档,图7-3展示了这两种控制器之间的关系。
MVC应用程序设计指南(Design Guidelines for MVC Applications)
下列准则适用于应用程序设计中的模型视图控制器的注意事项:
*虽然你可以使用一个自定义NSObject的子类的实例作为中介控制器,但是没有理由去因为要完成所有的工作而来创建这样一个。而是使用一个现成的为Cocoa绑定技术而设计的NSController对象;即使用NSObjectController, NSArrayController, NSUserDefaultsController, 或 NSTreeController的实例或这些NSController具体子类的自定义子类具体子类。
但是,如果应用程序非常简单,你觉得能很轻松的书写实现中介行为(使用outlets 和 target-action)所需要的胶水代码,可以随意使用自定义NSObject子类的一个实例作为中介控制器。在自定义NSObject的子类中,你也可以就NSController来实现调节控制器,利用键值编码,键值观察,和编辑协议。
*虽然你可以在一个对象中结合MVC角色,最好的策略是保持角色之间的分离。这种分离提高了程序中对象的可重用性和可扩展性。如果你打算在一个类中结合MVC角色,给这个类选择一个主要的角色,然后(用于维护目的)在相同的实现文件中使用类扩展来扩展这个类来扮演其他的角色。
*一个设计良好的MVC应用程序的一个目标应该是使用尽可能多的对象,那样(理论上,至少)可重用。特别的,视图对象和模型对象应该是高度可重用的。(当然,现成的中介控制器对象是可重用的)应用程序特定的行为往往尽可能集中在控制器对象中。
*虽然让视图直接观察模型来监听状态的变化是可行的,最好不要这样做。一个视图对象应该总是通过一个中介控制器对象来了解模型对象的更改。有两方面的原因:
-如果你使用绑定机制来让视图对象直接观察模型对象的属性,你忽视了NSController及其子类给你的应用的所有的优点:选择器与占位符管理以及提交和放弃改变的能力。
-如果您不使用绑定机制,您必须子类化一个存在的视图类,以添加观察由模型对象抛出的改变的通知的能力。
*努力限制应用程序的类中的代码依赖性。一个类在另一个类上的依赖性越大,它的可重用性就越少。具体建议如下:
-视图类不应该依赖于模型类(虽然这可能是不可避免的一些自定义视图)。
-视图类不应该依赖于一个中介控制器类。
-一个模型类不应该依赖任何其他模型类。
-一个中介控制器类不应该依赖于一个模型类(虽然,如视图,如果它是一个自定义控制器类这可能是必要的)。
-一个中介控制器类不应该依赖于视图类或协调控制器类。
-一个协调控制器类依赖于所有MVC角色类型的类。
*如果Cocoa提供解决编程问题的架构,这个架构给特定类型的对象分配MVC角色。如果你这样做的话,把你的项目放在一起会容易得多。例如,文档结构,包括一个配置NSDocuments对象的Xcode项目模板(每个nib模型控制器)作为文件的所有者。
Cocoa(OS X)中的模型视图控制器(Model-View-Controller in Cocoa (OS X))
模型视图控制器的设计模式是许多Cocoa机制和技术的基础。因此,使用MVC设计模式在面向对象设计的重要性超越了使应用达到更大的可重用性和可扩展性。如果你的应用是融合了基于MVC模式的Cocoa技术,如果你程序的设计也遵循MVC模式,那么它将工作得最好。它使用这些技术将会有相对少的弊端如果你的程序有个好的MVC分离,但是你如果你没有一个良好的分离的话你将会付出更多的努力来使用这个技术。
在OS X操作系统中Cocoa包括以下这些架构基于模型视图控制器的架构、机制和技术:
*Document architecture.在这种体系结构中,一个基于文档的应用包含一个相关整个应用程序的控制器对象(NSDocumentController),一个关于每个文档窗口的控制器对象(NSWindowcontroller),和一个将每个文档的控制器和模型角色联系起来的对象(NSDocuments)。
*Bindings.MVC是Cocoa绑定技术的核心。抽象类NSController的具体子类提供现成的控制器对象,您可以配置它们来建立视图对象和设计合理的模型对象之间的绑定。
*Application scriptability.在设计应用程序时使它可脚本化,它不仅需要遵循MVC设计模式,并且,你的应用程序的模型对象要设计合理。访问应用程序状态和请求应用程序行为的脚本命令通常应该发送到模型对象或控制器对象中。
*Core Data.Core Data框架管理模型对象的图表,并确保这些对象的持久性,通过保存它们(并检索它们)到一个持久性存储。Core Data和Cocoa技术是紧密结合的。MVC设计模式和对象建模是Core Data架构的基本决定因素。
*Undo.在“撤消架构”中,模型对象再次发挥核心作用。模型对象的原始方法(通常是它的访问器方法)往往是你实现撤消和重做操作的地方。视图和控制器对象的操作也可能涉及这些操作;例如,您可能有这样的对象给“撤消”和“重做”菜单项提供特定的标题,也可能让它们在文本视图中撤消选择项。