Cocoa环境中的许多体系结构和机制都有效地利用了设计模式:可以解决特定情况下反复出现的问题的抽象设计. 本章描述了Cocoa中设计模式的主要实现, 尤其侧重于模型-视图-控制器(MVC)和对象建模. 本章的主要目的是为了更加了解Cocoa中的设计模式, 并鼓励在自己的软件项目中利用这些模式
What Is a Design Pattern?
设计模式是用于设计的模板, 可以解决特定情况下的普遍性重复性问题. 它是一种抽象工具, 在诸如体系结构和工程以及软件开发等领域非常有用. 以下各节总结了什么是设计模式, 解释了为什么它们对面向对象设计很重要, 并介绍了示例设计模式
A Solution to a Problem in a Context
作为开发人员, 可能已经熟悉面向对象编程中的设计模式概念. 它们首先由Erich Gamma, Richard Helm, Ralph Johnson和John Vlissides(通常称为"四人帮")在《设计模式:可重用的面向对象软件的元素》中进行了权威描述和分类. 该书最初于1994年出版, 不久之后又有其他书和文章, 进一步探讨和阐述了面向对象系统中的设计模式
对设计模式的简洁定义是"一种在上下文中解决问题的方法". 让我们通过回顾该短语来对此进行解析. 上下文是应用该模式的重复出现的场景. 问题是图此上下文中实现的目标以及上下文带来的任何约束. 解决方案就是要追求的:针对上下文的通用设计, 可以实现目标并解决约束.
设计模式将具体设计的结构的关键方面抽象化, 该结构已被证明是有效的. 该模式具有名称, 并标识参与该模式的类和对象及其职责和协作. 它还阐明了后果(成本和收益)以及可以应用该模式的情况. 设计模式是一种用于特定设计的模板或指南. 从某种意义上说, 具体的设计是模式的"实例化". 设计模式不是绝对的, 可以灵活地应用它们, 诸如编程语言和现有体系结构之类的东西通常可以确定模式的应用方式
设计的几个主题或原则会影响设计模式. 这些设计原则是构建面向对象系统的经验法则. 例如"封装变化的系统结构的各个方面"和"对接口进行编程, 而不是对实现进行编程". 它们表达了重要的见解. 例如如果隔离系统的各个部分并封装它们, 则它们可以独立于系统的其他部分而变化, 尤其是在为它们定义与实现细节无关的接口时. 以后可以更改或扩展这些可变部分, 而不影响系统的其他部分. 因此可以消除了依赖性, 并减少了组件之间的耦合, 系统变得更加灵活更易于更改
这些好处使设计模式成为在编写软件时的重要考虑因素. 如果在程序的设计中找到, 改版和使用模式, 则该程序及其组成的对象和类将更易重用更易扩展, 并且在将来的需求需要时更易于修改. 而且基于设计模式的程序通常比不基于设计模式的程序更优雅更高效, 为它们需要更少的代码行就能实现相同的目标
An Example: The Command Pattern
《四人帮》一书的大部分内容都是设计模式的目录. 它按范围(类或对象)和目的(创建,结构或行为)对目录中的模式进行分类. 目录中的每个条目都讨论设计模式的意图, 动机, 适用性, 结构, 参与者, 协作, 结果和实现. 其中有一个是命令模式(对象行为模式)
该书将命令模式的目的描述为"将请求封装为对象, 从而可以将具有不同请求, 队列或日志请求的客户端参数化, 并支持可撤销的操作". 该模式将对象发送消息与对象分离接收并评估这些消息. 消息的发起方(客户端)通过将特定接收方上的一个或多个操作绑定在一起来封装请求. 封装的消息可以在对象之间传递, 放置在队列中或以其他方式存储以供以后调用, 并且可以动态修改以更改接收者或消息的参数
下图为该模式的结构图

对于熟悉Cocoa的开发人员来说, 对Command模式的简短概述可能会引起很大的反响. 该模式完美地描述了Foundation框架中的一个类, 该类的目的是封装消息:NSInvocation. 正如模式的意图所指出的那样其目的之一是使操作不执行. Invocation对象在Cocoa设计中用于撤消管理, distributed对象就是这样的对象, distributed对象是用于进程间通信的体系结构. 命令模式还描述了(虽然不太完美)Cocoa的target-action机制, 其中用户界面控制对象封装了用户触发的target和action消息
在其框架类,语言和运行时中, Cocoa已经为开发者实现了许多设计模式. 通过使用设计模式的这些"现成"改版版本之一, 可以满足许多开发要求. 或者可以考虑自己的问题, 并且问题的上下文要求自己进行基于模式的全新设计. 重要的是在开发软件时要注意模式并在适当的时候在设计中使用它们
How Cocoa Adapts Design Patterns
可以在Cocoa的OS X和iOS版本中找到各种设计模式的改版. 基于模式的机制和架构在Cocoa框架以及Objective-C运行时和语言中很常见. Cocoa经常将自己的特性放在模式上, 因为其设计受语言能力或现有体系结构等因素的影响
本节包含"设计模式:可重用的面向对象软件的元素"中分类的大多数设计模式的摘要. 每个部分不仅总结了该模式, 还讨论了该模式的Cocoa实现. 仅列出了Cocoa实现的模式, 以下各节中对模式的每个描述都与特定的Cocoa上下文有关
Cocoa中设计模式的实现有各种形式. 以下各节中描述的一些设计(如协议和分类)是 Objective-C语言的功能. 在其他情况下, "模式实例"在一个类或一组相关类(例如类簇和单例类)中实现. 在其他情况下, 模式适应是一个主要的框架体系结构, 如响应者链. 一些基于模式的机制, 你得到几乎"免费", 而其他需要一些工作, 你的一部分. 即使 Cocoa 不实施一种模式, 当情况需要时, 也鼓励自己这样做. 例如, 对象组合(修饰器模式)通常比子类扩展类行为更好的技术
模型-视图-控制器(MVC)和对象模型这两种模式将在后面介绍. MVC是复合或聚合模式, 这意味着它基于多个目录模式. 对象模型在"四人帮"目录中没有对应项, 而是源自关系数据库的域. 然而MVC和对象模型可能是Cocoa中最重要、最普遍的设计模式. 而且在很大程度上它们是相互关联的模式. 它们在设计多种技术(包括绑定、撤消管理、脚本编写和文档体系结构)方面发挥着至关重要的作用
Abstract Factory 抽象工厂
抽象工厂模式提供了一个接口, 用于创建相关对象或从属对象, 而不指定其具体类. 客户端与从工厂获取的指定的具对象是分离的
Class Cluster 类簇
类簇是在公共抽象超类下对多个私具体子类进行分组的体系结构. 抽象超类声明创建其私有子类实例的方法. 超类根据调用的创建方法分配适当的具体子类的对象. 返回的每个对象可能属于不同的私有具体子类
Cocoa中的类簇只能生成其数据存储可能因情况而异的对象. 基础框架具有NSString, NSData, NSDictionary, NSSet, NSArray. 以及NSMutableString, NSMutableData, NSMutableDictionary, NSMutableSet, NSMutableArray
用途和局限性
如果要创建类簇表示的类型的不可变或可变对象, 请使用类簇的公共类接口. 对于类簇, 在简单性和可扩展性之间有一个权衡. 类簇简化了类的接口从而更易于学习和使用类. 但是创建类簇的抽象超类的自定义子类通常比较困难
Adapter 适配器
适配器设计模式将类的接口转换为客户端期望的另一个接口. 适配器可以让类和类之间协同工作. 它将客户端与目标对象的类分离
Protocols 协议
协议是一种语言级(Objective-C)特性, 协议定义的接口就可以作为适配器模式实例. 协议本质上是一系列与类无关的方法声明. (在Java中, 接口是协议的同义词) 如果希望客户端对象与另一个对象通信, 但对象的不兼容接口使这一点变得困难, 则可以定义协议. 然后另一个对象的类正式采用协议, 并通过实现协议的一个或多个方法"符合"协议. 该协议可能要求符合类来实现其某些方法, 并可能将其他方法的实现保留为可选. 然后客户端对象可以通过协议接口向另一个对象发送消息
协议创建一组独立于类继承结构的方法声明. 它们使得根据协议(像类的继承一样)对象进行分组成为可能. NSObject的conformsToProtocol:方法可以用来验证对象是否遵从某个协议
Cocoa有非正式的协议和正式的协议. 非正式协议是NSObject类上的分类, 使得使任何对象成为该分类中任何方法的潜在实现者. 非正式协议中的方法可以有选择地实现. 非正式协议是OS X中代理制实现的一部分
请注意协议的设计与适配器模式的描述不完全匹配. 但它实现了模式的目标:允许具有不兼容接口的类协同工作.
用途和局限性
使用协议声明接口, 如果没有继承关系的类之间要进行通信, 则需要通信的类要遵从协议, 实现协议的接口. Cocoa框架包括许多正式协议, 使自定义子类能够出于特定目通信. 例如NSObject的NSCopying, NSCoding协议, AppKit的NSDraggingInfo, NSTextInput, NSChangeSpelling协议, UIKit的UITextInputTraits, UIWebViewDelegate, UITableViewDataSource等
正式协议默认遵守协议的类要实现协议所有的接口. 也可以使用@optional将接口声明为可选的. 协议也是脆弱的, 如果将来协议进行了修改(除非是可选协议接口), 则会破会之前继承协议的类
Chain of Responsibility 责任链
责任链设计模式通过给多个对象处理请求的机会, 将请求的发送者与其接收方分离. 模式将接收对象链接在一起, 并沿链传递请求, 直到对象处理它. 链中的每个对象要么处理请求, 要么将其传递给链中的下一个对象.
Responder Chain 响应者链
应用程序框架包括称为响应者链的体系结构. 此链由一系列响应对象(即从NSResponder继承的对象, 或在UIKit中UI响应者)组成, 通过该对象传递事件(例如单击鼠标)或操作消息, 并最终处理. 如果给定的响应对象不处理特定消息, 它将消息传递给链中的下一个响应者. 链中的响应对象的顺序通常由视图层次结构确定, 层次结构中从较低级别响应者到更高级别的响应者的顺序, 最终由管理视图层次结构的窗口对象(即窗口对象或全局应用程序对象)响应. 响应者链上的事件和操作消息的路径是不同的. 应用程序和窗口(甚至视图的本地层次结构)有一样多的响应者链, 但一次只能有一个响应者链处于活动状态 -- 与当前活动窗口关联的响应者链
AppKit框架还实现了类似的响应者链用于错误处理
注意 UIKit与AppKit不同, 实现了响应者链. 如果视图由UIViewController对象管理, 则视图控制器将成为链中的下一个响应者(事件或操作消息从那里传递到视图的superview). 此外UIKit本身不支持文档体系结构, 因此响应者链中没有文档对象或窗口控制器对象. iOS中也没有错误处理响应者链
与响应者链密切相关的视图层次结构的使用的设计是复合模式. 操作消息(来自控件对象的消息)基于target-action机制, 这是命令模式一个的实例
用途和局限性
当使用界面生成器或以编程方式为程序构建用户界面时, 将获得一个或多个"免费"响应者链. 响应者链与视图层次结构齐头并进, 当使视图对象成为窗口内容视图的子视图时, 会自动获取该层次结构. 如果将自定义视图添加到视图层次结构中, 则该视图将成为响应者链的一部分. 如果实现适当的 NSResponder或UIResponder方法, 则可以接收和处理事件和操作消息. 作为窗口对象或全局应用程序对象的代理的自定义对象(AppKit中的NSApp)也可以接收和处理这些消息
还可以以编程方式将自定义响应方注入响应者链, 并且可以以编程方式操作响应方的顺序
Command 命令
命令设计模式将请求封装为对象, 从而允许使用不同的请求、队列或日志请求对客户端进行参数化, 并支持可撤消的操作. 请求对象将特定接收方上的一个或多个操作绑定在一起. 命令模式将发出请求的对象与接收和执行该请求的对象分开
Invocation Objects
NSInvocation的实例封装了Objective-C消息. 调用对象包含目标对象、方法选择器和方法参数. 可以动态更改调用对象调度的消息的目标及其参数. 执行调用后还可以从对象获取返回值. 使用一个invocation对象, 可以重复调用目标和多个参数的消息
NSInvocation对象的创建还需要一个NSMethodSignature对象, 该对象是封装与方法的参数和返回值相关的类型信息的对象. 相反, 可以通过方法选择器创建NSMethodSignature对象. NSInvocation的实现也用到了Objective-C的运行时函数
用途和局限性
NSInvocation对象是分布式对象、撤消管理、消息转发和计时器的编程接口的一部分. 还可以在类似需要将发送消息的对象与接收消息的对象分离的上下文中使用NSInvocation对象
The Target-Action Mechanism
target-action机制使控件对象(如按钮、滑块或文本字段), 能够将消息发送到另一个对象, 该对象可以解释消息并将其处理为特定于应用程序的指令.
接收对象或目标通常是自定义控制器对象. 消息(名为操作action消息)由选择器确定, 该选择器是方法的唯一运行时标识符.
在 AppKit 框架中, 控件拥有的单元格对象通常封装target和action. 当用户单击或以其他方式激活控件时, 控件将从其单元格中提取信息并发送消息(菜单项封装目标和操作, 并在用户选择时发送操作消息). target-action机制可以基于选择器(而不是方法签名)工作, 因为根据约定在AppKit中操作方法的签名始终相同
在UIKit中, target-action机制不依赖于单元格. 相反控件将目标和操作映射到控件上可能发生的一个或多个多点触控事件.
用途和局限性
创建Cocoa应用程序时, 可以通过应用程序界面构造器(Interface Builder)设置控件的操作和目标. 因此可以让控件启动自定义行为, 而无需为控件本身编写任何代码. 操作选择器和目标连接归档在nib件中, 并在nib文件解档时还原. 还可以通过发送控件或其单元格 setTarget:和setAction:消息来动态更改目标和操作
OS X的Cocoa应用程序可以使用target-action机制指示自定义控制器对象将数据从用户界面传输到模型对象或在模型对象中显示数据. Cocoa绑定技术不需要用此机制来更新数据
控件和单元格(cell)不拥有(retain)他们的targets
Composite 复合
复合设计模式将相关对象组合成树形结构, 以表示部分整体层次结构. 该模式使客户可以统一对待单个对象和对象组.
复合模式是Model-View-Controller聚合模式的一部分
View Hierarchy 视图层级
窗口中的视图(NSView或UIView对象)在内部构造为视图层次结构. 层次结构的根节点是一个窗口(NSWindow或UIWindow对象)及其内容视图是填充窗口内容矩形的透明视图. 添加到内容视图的视图成为该视图的子视图, 并且它们成为添加到它们的任何视图的父视图. 除内容视图之外, 一个视图具有一个(只有一个)父视图和零个或任意数量的子视图. 将这种结构视为:父视图包含其子视图
视图层次结构, 视图和结构

视图层次结构是一种结构体系结构, 在绘图和事件处理中都发挥着作用. 一个视图有两个边界矩形, 即frame和bounds, 这两个矩形会影响视图的图形操作方式. frame是外部边界, 它将视图放置在其父视图的坐标系中, 定义其大小, 然后将绘图裁剪到该视图的边缘. bounds(内部边界矩形)定义了视图在其上绘制的表面的内部坐标系
当窗口系统要求窗口为显示做准备时, 要求父视图在其子视图之前进行渲染. 当向视图发送一些消息时(例如请求视图重绘的消息), 该消息将传播到子视图. 因此可以将视图层次结构的分支视为统一视图
响应者链还使用视图层次结构来处理事件和操作消息
用途和局限性
只要以编程方式或使用界面构造器(Interface Builder)将视图添加到另一个视图, 就可以创建或修改视图层次结构. AppKit框架自动处理与视图层次结构关联的所有关系
Decorator 装饰器
装饰器设计模式动态地将附加职责到对象上. 装饰器为子类提供了灵活的替代方案, 以扩展功能. 与子类一样, 修饰器模式的改编允许合并新行为而无需修改现有代码. 装饰器包装其行为扩展的类的对象. 它们实现与包装对象相同的接口, 并在将任务代理给包装对象之前或之后添加自己的行为. 装饰器模式表达了设计原则, 即类应该对扩展开放, 而对修改不开放.
General Comments
装饰器是对象组合的一种模式, 建议在自己的代码中进行装饰. 但是Cocoa提供了自己的一些基于模式的类和机制(在以下各节中讨论). 在这些实现中, 扩展对象未完全复制其包装的对象的接口, 并且实现使用不同的技术进行接口共享.
Cocoa在其几个类的实现中使用装饰器了模式, 包括NSAttributedString, NSScrollView, UIDatePicker. 后两个类是复合视图的示例, 这些视图将其他视图类的简单对象组合在一起并协调它们的交互.
Delegation 代理
代理是一种机制, 宿主对象通过该机制将对另一个对象(它的代理)的弱引用(在某种意义上说是简单的指针引用, 没有保留)嵌入其中, 并在需要其任务输入时定期向该代理发送消息. 宿主对象通常是一个试图完成某事的"现成"框架对象(例如NSWindow或NSXMLParser对象), 但只能以通用方式完成. 代理几乎总是一个自定义类的实例, 该代理与宿主对象协同工作, 在任务的某些位置提供特定于程序的行为. 因此代理使得无需子类化即可修改或扩展另一个对象的行为
框架对象向其代理发送消息

从一个对象将一个代理任务给另一个对象的简单意义上讲, 代理是面向对象编程中的一种常见技术.但是, Cocoa以独特的方式执行代理. 宿主类使用正式协议或非正式协议来定义代理对象可以选择实现的接口. 非正式协议中的所有方法都是可选的, 正式协议可以声明可选方法, 从而允许代理仅实现协议中的某些方法. 在尝试向其代理发送消息之前, 宿主对象会确定它是否(通过responsToSelector:消息)实现了该方法, 以避免运行时异常
Cocoa框架中的某些类还将消息发送到其数据源. 数据源在所有方面都与代理相同, 除了意图是为宿主对象提供数据以填充browser, 表视图或类似的用户界面视图. 与代理不同, 可能还需要数据源来实现协议的某些方法
代理不是装饰器模式的严格实现. 宿主对象不会包装要扩展的类的实例, 实际上这是另一回事. 因为代理定制化代理框架类的行为, 除了框架类声明的代理方法外也没有共享接口.
Cocoa中的代理也是模板方法模式的一部分
用途和局限性
代理是Cocoa框架中常见的设计模式, AppKit, UIKit框架中的许多类都给代理发送消息, 例如NSApplication, UIApplication, UITableView以及NSView的一些子类. Foundation框架中的某些类(例如NSXMLParser和NSStream)也包含代理. 除非代理方法不允许实现目标, 否则应始终使用类的代理机制而不是对类进行子类化
尽管可以动态更改代理, 但是一次只能有一个对象成为代理. 因此如果希望同时向多个对象通知特定的程序事件, 则不能使用代理. 可以为此使用通知机制. 只要代理实现框架类声明的一个或多个通知方法, 代理就会自动从其代理框架对象接收通知
AppKit中的代理对象不保留(retain)其代理或数据源
Categories 分类
分类是Objective-C语言的一项功能, 无需创建类就可以向类添加方法(接口和实现). 在类的原始方法和分类添加的方法之间, 在程序范围内没有运行时差异. 分类中的方法成为类类型的一部分, 并被该类的所有子类继承
与代理一样, 分类并不是对装饰器模式的严格实现. 采用另外一种方式实现目标. 分类添加方法是编译时执行, 不是动态获取的. 而且分类不会封装正在扩展的类的实例
用途和局限性
Cocoa框架定义了许多分类, 其中大多数是非正式协议. 他们经常使用分类对相关方法进行分组. 可以在代码中实现分类, 以扩展类而无需子类化或对相关方法进行分组. 但是应该注意以下警告
- 不能将实例变量添加到类中
- 如果覆盖该类的现有方法则应用程序可能无法正常运行
Facade 外观
外观设计模式为子系统中的一组接口提供了统一的接口. 该模式定义了一个更高级别的接口, 该接口通过降低复杂性并隐藏子系统之间的通信和依赖性来使子系统更易于使用
NSImage
AppKit框架的NSImage类提供了一个统一的接口, 用于加载和使用基于位图的图像(例如JPEG, PNG或TIFF格式的图像)或基于矢量的图像(例如EPS或PDF格式的图像) NSImage可以保留同一图像的多个表示. 每个表示形式都是一种NSImageRep对象. NSImage自动选择适合于特定类型的数据和给定显示设备的表示形式. 它还隐藏了图像处理和选择的细节, 以便客户端可以互换使用许多不同的底层表示形式
用途和局限性
由于NSImage支持图像的几种不同表示, 因此某些请求的属性可能不适用. 例如, 如果基础图像表示基于矢量且与设备无关, 则向图像询问像素的颜色将不起作用
Iterator 迭代器
迭代器设计模式提供了一种在不暴露其基础表示的情况下顺序访问聚合对象(即集合)的元素的方法. 迭代器模式将访问和遍历集合元素的责任从集合本身转移到迭代器对象. 迭代器定义用于访问集合元素的接口并跟踪当前元素. 不同的迭代器可以执行不同的遍历策略
Enumerators
Foundation框架中的NSEnumerator类实现了Iterator模式. 抽象NSEnumerator类的私有的具体子类返回枚举器对象, 该对象依次遍历各种类型的集合(数组, 集合, 字典(值和键)), 将集合中的对象返回给客户端
NSDirectoryEnumerator是一个较远的类. 此类的实例递归枚举文件系统中目录的内容
用途和局限性
诸如NSArray, NSSet和NSDictionary之类的集合类包括一些方法, 这些方法返回适合于集合类型的枚举器. 所有枚举器都以相同的方式工作. 循环中的nextObject消息发送给枚举器对象, 该循环在枚举器返回nil退出
还可以使用快速枚举来访问集合的元素
Mediator 中间者
中间者设计模式定义了一个对象, 该对象封装了一组对象之间的交互方式. 中间者通过防止对象之间显式地相互引用来促进松散耦合, 并且可以独立地更改它们的交互. 这些对象因此可以保持更高的可重用性
这种模式下的"中间者对象"集中了系统中对象之间的复杂通信和控制逻辑, 这些对象在状态发生变化时告诉中间者对象, 并依次响应中间者对象的请求
AppKit框架中的控制器类
Model-View-Controller设计模式将角色分配给面向对象的系统(例如应用程序)中的对象. 它们可以是模型对象, 其中包含应用程序的数据并处理该数据. 它们可以是视图对象, 用于显示数据并响应用户的操作, 或者它们可以是控制器对象, 它们在模型和视图对象之间进行中介. 控制器对象适合中间者模式
在Cocoa中, 控制器对象可以是两种通用类型: 中间者控制器或协调控制器. 控制器控制应用程序中视图对象和模型对象之间的数据流. 控制器通常是NSController对象. 协调控制器为应用程序实现集中的通信和控制逻辑, 充当框架对象的代理和动作消息的目标. 它们通常是NSWindowController对象或自定义NSObject子类的实例. 由于它们非常专门用于特定程序, 因此协调控制器往往不可重用
AppKit框架中的抽象类NSController及其具体子类是Cocoa绑定技术的一部分, 该技术可自动同步模型对象中包含的数据以及视图对象中显示和编辑的数据. 例如如果用户在文本字段中编辑字符串, 则绑定技术会通过中介中间者控制器将该更改传达给绑定模型对象的适当属性. 所有程序员需要做的就是正确设计他们的模型对象, 并使用Interface Builder在程序的视图, 控制器和模型对象之间建立绑定
具体的公共控制器类的实例可从Interface Builder库中获得, 因此可以高度重用. 它们提供诸如选择和占位符值的管理之类的服务, 这些对象执行以下特定功能
- NSObjectController 管理单个model对象
- NSArrayController管理一系列model对象并维护, 它还允许向数组中添加对象或从数组中删除对象
- NSTreeController 可以在分层树结构中添加, 删除和管理model对象
- NSUserDefaultsController 偏好设置的便捷接口
用途和局限性
通常, 将NSController对象用作中介控制器, 因为这些对象旨在在应用程序的视图对象和模型对象之间传递数据. 要使用中介控制器, 通常需要从Interface Builder库中拖动对象, 指定模型对象属性键, 并使用界面生成器信息窗口的绑定面板在视图对象与模型对象之间建立绑定. 还可以将NSController或其子类之一作为子类, 以获得更定制的行为
只要这些对象符合NSKeyValueCoding和NSKeyValueObserving非正式协议, 就可以在几乎任何对象对之间进行绑定. 但是要获得NSController及其子类为带来的所有好处, 最好通过中介控制器进行绑定
协调控制器通过以下方式集中化应用程序中的通信和控制逻辑
- 维护用于
model和view对象的outlets(outlets是实例变量, 用于保存对其他对象的连接或引用) - 通过
target-action响应用户对视图对象的操纵 - 充当框架对象发送的消息的代理
通常可以在Interface Builder中建立以上所有连接 -- outlets, target-action, delegates. 然后将它们存储在应用程序的nib文件中
UIKit中的视图控制器
在iOS中运行的应用程序经常使用model和navigational user-interface设计来呈现应用程序填充数据的屏幕. 应用程序可能具有导航栏和工具栏, 并且在这些对象之间是应用程序数据的当前视图. 用户可以点击工具栏上的按钮以选择一种模式, 点击导航栏上的按钮, 并点击当前视图中的控件以遍历model(数据)对象的层次结构. 在每个层级, 中心视图都提供了更多细节. 在此层次结构的最后, 通常是用户可以检查或编辑的项目.
从UIViewController继承的视图控制器是此设计的核心. UIViewController是一个抽象类, 可以将其子类化以管理特定视图. UIKit框架还提供用于管理导航栏和工具栏对象的UIViewController子类:UINavigationController和UITabBarController. 如图所示, 一个tab-bar控制器可以管理多个导航控制器, 而导航控制器又可以管理一个或多个视图控制器, 每个视图控制器都具有关联的视图对象. 除了管理视图(包括叠加视图)之外, 视图控制器还指定在导航栏中显示的按钮和标题
UIKit中的视图控制器

Memento 备忘录
备忘录模式可捕获并具体化对象的内部状态(而不会违反封装), 以便以后可以将对象恢复为该状态. 备忘录模式将关键对象的重要状态保持在该对象之外, 以保持凝聚合
Archiving 归档
归档将程序中的对象以及这些对象的属性(属性和关系)转换为可以存储在文件系统中或在进程之间或通过网络传输的归档. 归档文件将程序的对象图捕获为与体系结构无关的字节流, 该字节流保留了对象的标识及其之间的关系, 由于对象的类型与数据一起存储, 因此从字节流中解码的对象通常使用与原始编码对象相同的类实例化
用途和局限性
通常, 要在程序中归档要保留其状态的那些对象. Model对象几乎总是属于此类. 通过编码将对象写入归档, 然后通过解码从归档中读取该对象. 编码和解码是使用NSCoder对象执行的操作, 最好使用键控归档技术(要求调用NSKeyedArchiver和NSKeyedUnarchiver类的方法). 被编码和解码的对象必须符合NSCoding协议, 在归档过程中调用此协议的方法
Property List Serialization 属性列表序列化
属性列表是对象图的简单结构化序列化, 仅使用以下类的对象:NSDictionary, NSArray, NSString, NSData, NSDate, NSNumber. 这些对象通常称为属性列表对象. 几种Cocoa框架类提供了一些方法来序列化这些属性列表对象, 并为记录对象内容及其层次关系的数据流定义特殊格式. NSPropertyListSerialization类提供了一些类方法, 这些类方法可将属性列表对象与XML格式或优化的二进制格式进行序列化
用途和局限性
如果对象图中的对象很简单, 则属性列表序列化是一种灵活, 可移植的方式, 可以捕获和具体化对象及其状态. 但是这种形式的序列化有其局限性. 它不保留对象的完整类标识, 而仅保留常规种类(数组, 字典, 字符串等). 因此, 从属性列表还原的对象可能与其原始类属于不同的类. 当对象的可变性可能变化时, 这尤其是个问题. 属性列表序列化也不跟踪对象中多次引用的对象, 反序列化时可能导致多个实例, 这些实例是原始对象图中的某个单例
Core Data
Core Data是一个Cocoa框架, 它定义了一种用于管理对象图并使之持久化的体系结构. 正是第二种功能(对象持久性)使Core Data成为备忘录模式的改版
在**Core Data体系结构中, 称为托管对象上下文的中心对象在应用程序的对象图中管理各种model对象. 在受管对象上下文的下方是该对象图的持久性堆栈-框架对象的集合, 这些对象在model对象和外部数据存储(例如XML文件或关系数据库)之间进行中介. 持久性堆栈对象在存储中的数据与托管数据上下文中的对应对象之间进行映射, 并且当存在多个数据存储时, 将它们作为单个聚合存储呈现给托管对象上下文.
Core Data的设计还受到模型-视图-控制器和对象建模模式的很深的影响
用途和局限性
Core Data在企业应用程序的开发中特别有用. 在该应用程序中必须定义, 管理, 透明地对model对象的复杂图形进行存储和从数据存储中解档. Xcode开发环境包括项目模板和设计工具, 这些模板和设计工具减少了创建两种通用类型的Core Data应用程序(基于文档的和非基于文档的)所需的编程工作. Interface Builder应用程序的库中还包含可配置的Core Data框架对象
Observer 观察者
观察者设计模式定义了对象之间的一对多依赖关系, 因此当一个对象改变状态时, 其所有依赖关系都会被通知并自动更新. 观察者模式本质上是一个发布和订阅模型, 其中主题及其观察者是松散耦合的. 可以在观察对象和被观察对象之间进行通信, 而无需彼此了解太多
Notifications 通知
Cocoa的通知机制基于观察者模式实现消息的一对多广播. 程序中的对象将自己或其他对象添加到一个或多个通知的观察者列表中, 每个通知都由全局字符串(通知名称)标识. 想要通知其他对象的对象(观察对象)创建一个通知对象, 并将其发布到通知中心. 通知中心确定特定通知的观察者, 并通过消息将通知发送给他们. 通知消息调用的方法必须符合特定的单参数签名. 该方法的参数是通知对象, 其中包含通知名称, 观察到的对象以及包含任何补充信息的字典.
发布通知是一个同步过程. 在通知中心将通知广播给所有观察者之前, 发布对象不会重新获得控制权. 对于异步行为, 可以将通知放在通知队列中. 否则, 可以将通知放入通知队列. 控件立即返回到发布对象, 并且通知中心在到达队列顶部时广播通知.
常规通知(即通知中心广播的通知)仅是进程内的. 如果要将广播广播到其他进程, 则可以使用分布式通知中心及其相关的API
用途和局限性
可以出于多种原因使用通知. 例如可以广播一个通知, 以根据程序中其他位置的特定事件来更改用户界面元素显示信息的方式. 或者可以使用通知来确保文档中的对象在关闭文档窗口之前保存其状态. 通知的一般目的是通知其他对象程序事件, 以便它们可以适当地响应.
但是接收通知的对象只能在事件发生后做出反应. 这与代理有很大的不同. 代理有机会拒绝或修改委托对象提出的操作. 另一方面, 观察对象不能直接影响即将发生的操作.
通知类是NSNotification(用于通知对象), NSNotificationCenter(用于发布通知和添加观察者), NSNotificationQueue(用于使通知入队)和NSDistributedNotificationCenter. 许多Cocoa框架类发布任何对象都可以观察到的通知
Key-Value Observing 键值观察
键值观察是一种机制, 它允许将其他对象的特定属性的更改通知给对象. 它基于NSKeyValueObserving非正式协议. 观察到的属性可以是简单属性, 一对一关系或一对多关系. 在"模型-视图-控制器"模式的上下文中, 键值观察尤为重要, 因为它使视图对象能够通过控制器层观察模型对象的变化. 因此, 它是Cocoa绑定技术的重要组成部分
Cocoa提供了许多NSKeyValueObserving方法的默认"自动"实现, 该方法为所有合规对象提供了属性观察功能
用途和局限性
键值观察类似于通知机制, 但在侧重方面有所不同. 在键值观察中, 没有中央对象为所有观察者提供更改通知. 而是将更改通知直接传输到观察对象. 键值观察也直接与特定对象属性的值相关. 另一方面, 通知机制更广泛地涉及程序事件.
参与键值观察(KVO)的对象必须符合KVO, 即符合某些要求. 对于自动观察, 这需要符合键值编码(符合KVC)的要求并使用符合KVC的方法(即访问器方法). 键值编码是一种用于自动获取和设置对象属性值的相关机制(基于相关的非正式协议).
可以通过禁用自动观察者通知并使用NSKeyValueObserving非正式协议和相关类别的方法来实现手动通知来优化KVO通知
Proxy 代理
代理设计模式为另一个对象提供了一个代理或占位符, 以控制对另一个对象的访问. 可以使用此模式来创建代表或代理对象, 该对象控制对另一个对象的访问, 该对象可能是远程的, 创建成本很高或需要保护. 这种模式在结构上与装饰者模式类似, 但是其用途不同. 装饰器向对象添加行为, 而代理控制对对象的访问
NSProxy
NSProxy类定义了对象的接口, 这些对象可充当其他对象(甚至尚不存在的对象)的替代. 代理对象通常将发送给它的消息转发到它所代表的对象, 但是它也可以通过加载所代表的对象或将其自身转换为该对象来响应该消息. 尽管NSProxy是一个抽象类, 但是它实现了NSObject协议和根对象期望的其他基本方法. 实际上就像NSObject类一样, 它是层次结构的根类.
NSProxy的具体子类可以完成代理模式的既定目标, 例如昂贵对象的延迟实例化或充当安全性的哨兵对象. NSDistantObject是Foundation框架中NSProxy的具体子类, 它实现了用于透明分布式消息传递的远程代理. NSDistantObject对象是分布式对象体系结构的一部分. 通过充当其他进程或线程中的对象的代理, 它们有助于启用那些线程或进程中的对象之间的通信.
NSInvocation对象是Command模式的一种改版, 也是分布式对象体系结构的一部分
用途和局限性
Cocoa只在分布式对象中使用NSProxy对象. NSProxy对象专门是具体子类NSDistantObject和NSProtocolChecker的实例. 不仅可以将分布式对象用于进程间消息传递(在同一台或不同计算机上), 还可以使用它来实现分布式计算或并行处理. 如果要将代理对象用于其他目的, 例如创建昂贵的资源或提高安全性, 则必须实现自己的NSProxy具体子类
Receptionist 接待员
接待员设计模式解决了将一个应用程序的一个执行上下文中发生的事件重定向到另一个执行上下文以进行处理的一般问题. 这是一种混合模式. 尽管它没有出现在"四人帮"一书中, 但它结合了该书中描述的命令, 备忘录和代理设计模式的元素. 它也是蹦床模式的变体(书中也没有出现), 在这种模式下, 蹦床对象最初会接收到一个事件, 之所以这么称呼, 是因为它会立即将该事件反弹或重定向到目标对象进行处理.
每当需要将工作重新启动到另一个执行上下文进行处理时, 都可以采用"接待员"设计模式. 当观察到通知, 实现块处理程序或响应操作消息并且想要确保代码在适当的执行上下文中执行时, 可以实现接待员模式将必须完成的工作重定向到该执行上下文. 使用接待员模式, 甚至可以在对任务进行处理以处理数据之前对传入的数据执行一些过滤或合并. 例如, 可以将数据收集成批, 然后间隔将这些批分派到其他地方进行处理.
接待员模式有用的一种常见情况是键值观察. 在键值观察中, 模型对象属性值的更改通过KVO通知传达给观察者. 但是, 可以在后台线程上对模型对象进行更改. 这会导致线程不匹配, 因为对模型对象状态的更改通常会导致对用户界面的更新, 并且这些更新必须在主线程上进行. 在这种情况下, 想将KVO通知重定向到主线程. 可以对应用程序的用户界面进行更新的位置
接待员设计模式实例
KVO通知会调用观察者实现的方法observeValueForKeyPath:ofObject:change:context:. 如果对属性的更改发生在辅助线程上, 则观察值observeValueForKeyPath:ofObject:change:context:代码在同一线程上执行. 在此模式中的中心对象, 接待员, 充当线程中介. 如图所示, 将接收对象指定为模型对象属性的观察者. 接待员实现观察值observeValueForKeyPath:ofObject:change:context:将从辅助线程上接收的通知重定向到另一个执行上下文- 主操作队列, 在这种情况下. 当属性发生变化时, 接待员会收到KVO通知. 接待员立即在主操作队列中添加块操作. 块包含由客户端指定的代码, 这些代码适当地更新用户界面
将KVO更新弹到主操作队列

定义接收器类, 以便它具有自身作为属性的观察者添加自身所需的元素, 然后将KVO通知转换为更新任务. 因此, 它必须知道它正在观察的对象、它所观察的对象的属性、要执行的更新任务以及要执行该对象的队列. 示例代码显示了RCReceptionist类及其实例变量的初始声明
声明接待员类
@interface RCReceptionist : NSObject {
id observedObject.
NSString *observedKeyPath.
RCTaskBlock task.
NSOperationQueue *queue.
}
RCTaskBlock实例变量是以下声明类型的块对象
typedef void (^RCTaskBlock)(NSString *keyPath, id object, NSDictionary *change).
用于创建接接待员的类工厂方法
+ (id)receptionistForKeyPath:(NSString *)path object:(id)obj queue:(NSOperationQueue *)queue task:(RCTaskBlock)task {
RCReceptionist *receptionist = [RCReceptionist new].
receptionist->task = [task copy].
receptionist->observedKeyPath = [path copy].
receptionist->observedObject = [obj retain].
receptionist->queue = [queue retain].
[obj addObserver:receptionist forKeyPath:path
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:0].
return [receptionist autorelease].
}
请注意, 代码复制块对象, 而不是保留它. 由于该块可能是在堆栈上创建的, 因此必须将其复制到堆中, 以便在传递KVO通知时存在于内存中
处理KVO通知
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context {
[queue addOperationWithBlock:^{
task(keyPath, object, change).
}].
}
此代码只需将任务排队到给定的操作队列中, 传递任务块观察到的对象、更改属性的key path以及包含新值的字典. 任务封装在NSBlockOperation中, 该对象在队列上执行任务
客户端对象提供在创建接收器对象时更新用户界面的块代码, 如下列代码所示. 当它创建接收对象时, 客户端将传递要在其中执行块的操作队列, 在这种情况下是主队列
创建接待员对象
RCReceptionist *receptionist = [RCReceptionist receptionistForKeyPath:@"value" object:model queue:mainQueue task:^(NSString *keyPath, id object, NSDictionary *change) {
NSView *viewForModel = [modelToViewMap objectForKey:model].
NSColor *newColor = [change objectForKey:NSKeyValueChangeNewKey].
[[[viewForModel subviews] objectAtIndex:0] setFillColor:newColor].
}].
Singleton 单例
单例设计模式可确保类只有一个实例, 并提供对它的全局访问点. 类跟踪其唯一实例, 并确保无法创建其他实例. 单例类适用于单个对象提供对全局资源的访问有意义的情况
Framework Classes 框架类
一些Cocoa框架类是就是单例. 它们包括NSFileManager, NSWorkspace, NSApplication, UIApplication. 进程仅限于这些类的一个实例. 当客户端请求类提供实例时, 它将获取一个共享实例, 该实例在第一个请求时是懒加载创建的.
用途和局限性
使用单例类返回的共享实例与使用非单例类的实例没有什么不同, 只是阻止复制、保留或释放它(相关方法重新实现为空操作). 如果情况需要, 可以创建自己的单例类
Template Method 模板方法
模板方法设计模式定义操作中算法的骨架, 将一些步骤推迟到子类. 模板方法模式允许子类重新定义算法的某些步骤, 而无需更改算法的结构
重写的框架方法
模板方法模式是Cocoa的基本设计, 实际上是面向对象框架的基本设计. Cocoa中的模式允许程序的自定义组件将自己挂钩到算法中, 但框架组件确定何时以及如何需要它们. Cocoa类的编程接口通常包括要由子类重写的方法. 在运行时, 框架在所执行的任务中的某些点调用这些所谓的泛型方法. 泛型方法为自定义代码提供了一个结构, 用于将特定于程序的行为和数据贡献给框架类执行和协调的任务
用途和局限性
若要利用Cocoa版本的模板方法模式的, 必须创建一个子类并重写框架调用的那些方法, 以将特定于应用程序的输入插入到正在执行的算法中. 如果正在编写自己的框架, 则可能应在设计中包括该模式
OS X中的文档体系结构
AppKit框架定义的文档体系结构是重写框架方法的一般设计的一个特定且重要的实例, 作为模板方法模式的适应. Cocoa应用程序可以创建和管理多个文档, 每个文档在其自己的窗口中, 几乎总是基于文档体系结构. 在此体系结构中, 有三个框架类的合作对象:NSDocument、NSWindowController 和NSDocumentController. NSDocument对象管理表示文档数据的模型对象. 根据用户请求, 他们将该数据写入文件并重新加载数据, 并使用它重新创建模型对象. NSWindowController对象管理特定文档的用户界面. 基于文档的应用程序的 NSDocumentController对象跟踪和管理所有打开的文档, 否则将协调应用程序的活动. 在运行时, 每个对象都从 AppKit接收消息, 请求它执行特定操作. 应用程序开发人员必须重写这些消息调用的许多方法, 以添加特定于应用程序的行为.
Cocoa文档体系结构的设计也受到模型-视图-控制器模式的很深的影响
用途和局限性
可以通过从Xcode中的"新项目"对话框中选择基于Cocoa文档的应用程序模板, 为基于文档的Cocoa应用程序创建项目. 然后必须实现NSDocument的自定义子类, 并可以选择实现NSWindowController和 NSDocumentController的自定义子类. AppKit框架提供了应用程序的大部分文档管理逻辑
The Model-View-Controller Design Pattern
模型-视图-控制器设计模式(MVC)相当古老. 少早期开始, 它的变化就已经存在了. 它是一种高级模式, 因为它与应用程序的全局体系结构有关, 并根据对象在应用程序中的一般角色对对象进行分类. 它还是一个复合模式, 因为它包括几个或者更多的元素模式.
面向对象程序通过调整其设计的MVC设计模式, 从多种方式受益. 这些程序中的许多对象往往更可重用, 并且其接口往往定义得更好. 总体而言, 这些程序更适应不断变化的需求, 换句话说, 它们比不基于MVC的程序更容易扩展. 此外Cocoa中的许多技术和体系结构(如绑定、文档体系结构和脚本能力)都基于MVC, 并要求自定义对象扮演MVC定义的角色之一
Roles and Relationships of MVC Objects
MVC设计模式认为有三种类型的对象:模型对象、视图对象和控制器对象. MVC模式定义这些类型的对象在应用程序及其通信线路中扮演的角色. 在设计应用程序时, 主要步骤是选择或为属于这三个组之一的对象创建自定义类. 这三种对象中的每一种都通过抽象边界与其他对象分开, 并与跨越这些边界的其他类型的对象进行通信
Model Objects Encapsulate Data and Basic Behaviors
模型对象封装数据和基本行为
模型对象代表特殊知识和专业知识. 它们保存应用程序的数据并定义操作该数据的逻辑. 设计良好的MVC应用程序将其所有重要数据封装在模型对象中. 作为应用程序持久状态的一部分(无论该持久状态是否存储在文件或数据库中)的任何数据都应驻留在模型对象中, 一旦将数据加载到应用程序中. 因为它们代表与特定问题领域相关的知识和专业知识, 因此它们往往是可重用的.
理想情况下, 模型对象与用于呈现和编辑它的用户界面没有显式连接. 例如, 如果有一个表示某人的模型对象(例如正在编写通讯簿), 则可能需要存储出生日期. 这是一件好事, 存储在人模型对象. 但是存储日期格式字符串或其他有关如何显示该日期的信息可能在其他地方更好.
实际上, 这种分离并不总是最好的, 这里还有一些灵活性的空间, 但一般来说, 模型对象不应该关注接口和表示问题. 一个合理的示例是具有表示显示图形的模型对象的图形应用程序. 图形对象知道如何绘制自己是有意义的, 因为它们存在的主要原因是定义视觉对象. 但是, 即使在这种情况下, 图形对象(graphic objects)也不应依赖于存活在特定的视图或任何视图中, 它们也不应负责知道何时绘制自己. 应该要求他们通过视图对象(view object)来绘制它们
View Objects Present Information to the User
视图对象向用户显示信息
视图对象知道如何显示并可能允许用户编辑应用程序模型中的数据. 视图不应负责存储它显示的数据. (当然,这并不意味着视图从未实际存储它显示的数据. 出于性能原因, 视图可以缓存数据或执行类似的技巧). 视图对象可以只显示模型对象的一部分, 或整个模型对象, 甚至许多不同的模型对象. 视图有许多不同的种类.
视图对象往往是可重用和可配置的, 它们提供应用程序之间的一致性. 在Cocoa中, AppKit框架定义了大量视图对象, 并在接口生成器库中提供了其中的许多对象. 通过重用AppKit的视图对象(如NSButton对象), 可以保证应用程序中的按钮的行为与任何其他Cocoa应用程序中的按钮一样, 从而确保应用程序的外观和行为高度一致.
视图应确保正确显示模型. 因此,它通常需要了解模型的更改. 由于模型对象不应绑定到特定的视图对象, 因此它们需要一种通用方法来指示它们已更改.
Controller Objects Tie the Model to the View
控制器对象将模型连接到视图
控制器对象充当应用程序的视图对象与其模型对象之间的中介. 控制器通常负责确保视图能够访问它们需要显示的模型对象, 并作为视图了解模型更改的管道. 控制器对象还可以为应用程序执行设置和协调任务, 并管理其他对象的生命周期.
在典型的Cocoa MVC设计中, 当用户通过视图对象输入值或指示选择时, 该值或选择将传达给控制器对象. 控制器对象可能以某种特定于应用程序的方式解释用户输入, 然后告诉模型对象如何处理此输入, 例如, "添加新值"或"删除当前记录", 或者模型对象可能反映已更改的值在其属性之一. 基于相同的用户输入, 某些控制器对象可能还会告诉视图对象更改其外观或行为的方面, 例如告诉按钮禁用自身. 相反, 当模型对象发生更改时(例如, 访问新的数据源时), 模型对象通常将更改传达到控制器对象, 然后该控制器对象请求一个或多个视图对象相应地更新自身.
控制器对象可以是可重用的, 也可以是不可重用的, 具体取决于其常规类型.
Combining Roles
组合角色
可以合并对象扮演的MVC角色, 例如,使对象同时满足控制器和视图角色, 在这种情况下, 它将称为视图控制器. 同样,还可以具有模型控制器对象. 对于某些应用程序, 组合这样的角色是可接受的设计.
模型控制器(model controller)是主要与模型层有关. 它"拥有"模型, 其主要职责是管理模型并与视图对象进行通信. 应用于整个模型的操作方法通常在模型控制器中实现. 文档体系结构提供了许多这些方法. 例如,文档体系结构提供了以下许多方法. 例如NSDocument对象(文档体系结构的中心部分)会自动处理与保存文件相关的操作方法.
视图控制器(view controller)是主要与视图层有关. 它"拥有"界面(视图), 其主要职责是管理接口并与模型通信. 与视图中显示的数据相关的操作方法通常在视图控制器中实现. NSWindowController对象(也是文档体系结构的一部分)是视图控制器的示例.
Types of Cocoa Controller Objects
"控制器对象绑定模型和视图"部分将模型与视图关联可绘制控制器对象的抽象轮廓, 但实际上图片要复杂得多. 在Cocoa中有两种一般类型的控制器对象: 中介控制器(Mediating controllers)和协调控制器(coordinating controllers). 每种控制器对象都与一组不同的类相关联, 每个类提供不同的行为范围.
中介控制器通常是从NSController类继承的对象. 在Cocoa绑定技术中使用中介控制器对象. 它们促进或调解视图对象和模型对象之间的数据流
中介控制器通常是从Interface Builder库拖动的现成对象. 可以配置这些对象, 以建立视图对象属性和控制器对象属性之间的绑定, 然后在这些控制器属性和模型对象的特定属性之间建立绑定. 因此当用户更改视图对象中显示的值时, 新值将自动传达到模型对象以进行存储 — 通过中介控制器. 当模型的属性更改其值时, 该更改将传达到视图以进行显示. 抽象NSController类及其具体子类 — NSObjectController, NSArrayController, NSUserDefaultsController, NSTreeController — 提供支持功能, 例如提交和放弃更改的能力以及选择和占位符值.
协调控制器通常是NSWindowController, NSDocumentController对象(仅在AppKit中可用)或NSObject的自定义子类的实例. 它在应用程序中的角色是监督或协调整个应用程序或应用程序部分(如从nib文件解档的对象)的功能. 协调控制器提供的服务:
- 相应代理消息(delegation)和观察的通知
- 相应action消息
- 管理拥有对象的生命周期(例如在适当的时间释放它们)
- 在对象之间建立连接并执行其他设置任务
NSWindowController和NSDocumentController是有关基于文档的应用程序的Cocoa体系结构的一部分的类. 这些类的实例为上面列出的几个服务提供默认实现, 可以创建它们的子类来实现更多特定于应用程序的行为. 甚至可以使用NSWindowController对象来管理应用程序中不基于文档体系结构的窗口.
协调控制器通常拥有在nib文件中归档的对象. 作为文件的所有者, 协调控制器是nib文件中的对象的外部, 并管理这些对象. 这些拥有的对象包括中介控制器以及窗口对象和视图对象. 有关将控制器作为文件所有者进行协调的更多信息
自定义NSObject子类的实例可以完全适合作为协调控制器. 这些类型的控制器对象结合了中介和协调功能. 对于中介行为, 他们利用target-action, outlets, delegation, notifications等机制, 促进视图对象和模型对象之间的数据移动. 它们往往包含大量的粘性代码, 并且由于该代码是特定于应用程序的, 因此它们是应用程序中可重用最少的对象类型
MVC as a Compound Design Pattern
模型-视图-控制器是一种设计模式, 由几个更基本的设计模式组成. 这些基本模式协同工作, 以定义MVC应用程序特有的功能分离和通信路径. 但是MVC的传统概念分配了一组不同于Cocoa分配的基本模式. 区别主要在于提供给控制器的角色和查看应用程序的对象.
在原始(Smalltalk)概念中, MVC由复合、策略和观察者模式组成
- 复合
- 应用程序中的视图对象实际上是嵌套视图的复合体, 这些视图以协调的方式(即视图层次结构)协同工作. 这些显示组件的范围从窗口到复合视图(如表视图)到单个视图(如按钮). 用户输入和显示可以在复合结构的任何级别进行
- 策略
- 控制器对象实现一个或多个视图对象的策略. 视图对象仅限于维护其可视方面, 并将应用程序有关界面行为的特定含义的所有决策委托给控制器
- 观察
- 模型对象在应用程序中观察感兴趣的对象(通常
view视图对象)以便更改自己状态的
- 模型对象在应用程序中观察感兴趣的对象(通常
下图描述了复合、策略和观察者模式协同工作的传统方式:用户在复合结构的某个层级操作视图, 从而生成事件. 控制器对象接收事件, 并以特定于应用程序的方式解释它, 即应用了策略. 此策略可以是请求(通过消息)模型对象更改其状态, 或请求视图对象(在复合结构的某个级别)更改其行为或外观. 反过来模型对象会通知在其状态更改时注册为观察者的所有对象. 如果观察者是视图对象, 它可以相应地更新其外观
传统版本的复合模式MVC

作为复合模式的Cocoa版本与传统版本有一些相似之处, 实际上, 基于上图中的图表构建工作应用程序是完全可能的. 通过使用绑定技术, 可以轻松地创建Cocoa MVC应用程序, 其视图直接观察模型对象以接收状态更改通知. 但是这种设计存在理论问题. 视图对象和模型对象应该是应用程序中最可重用的对象. 视图对象表示操作系统和系统支持的应用程序的"外观". 外观和行为的一致性至关重要, 这需要高度可重用的对象. 按定义对对象建模, 封装与问题域关联的数据, 并对该数据执行操作. 在设计方面, 最好保持模型和视图对象彼此分开, 因为这可以增强对象的可重用性.
在大多数Cocoa应用程序中, 模型对象中的状态更改通知通过控制器对象传达以视图对象. 下图显示了这种不同的配置, 尽管涉及两种更基本的设计模式, 但该配置看起来更简洁
Cocoa版本的复合模式MVC

此复合设计模式中的控制器对象包含中介模式和策略模式. 它调解模型和视图之间的数据流, 并在模型视图两个方向上查看对象. 模型状态的更改通过应用程序的控制器对象进行通信到视图对象. 此外视图对象通过target-action机制的实现组合命令模式.
注意 target-action机制使视图对象能够传达用户输入和选择, 可以在协调和中介控制器对象中实现. 但是, 该机制的设计因控制器类型而异. 对于协调控制器, 将视图对象连接到Interface Builder中的目标对象(控制器对象), 并指定必须符合特定签名的操作选择器(action selector). 协调控制器, 由于作为窗口和全局应用程序对象的委托, 也可以处于响应器链中. 中介控制器使用的绑定机制还将视图对象连接到目标, 并允许具有任意类型参数可变数的操作签名. 但是中介控制器不在响应器链中.
上图中描述的修正后的复合设计模式有实际原因和理论原因, 特别是当涉及到中介设计模式时. 中介控制器派生自NSController的具体子类, 这些类除了实现中介模式外, 还提供许多应用程序应该利用的功能, 例如选择和占位符值的管理. 如果选择不使用绑定技术, 视图对象可以使用一种机制, 如Cocoa通知中心来接收来自模型对象的通知. 但是这将要求创建一个自定义视图子类来添加模型对象发布的通知的知识.
在设计良好的Cocoa MVC应用程序中, 协调控制器对象通常拥有中介控制器, 这些控制器在nib文件中归档. 下图显示了两种控制器对象之间的关系
协调控制器作为nib件的所有者

Design Guidelines for MVC Applications
以下准则适用于应用程序设计中的模型-视图-控制器注意事项
-
尽管可以使用NSObject的自定义子类的实例作为中介控制器, 但没有理由完成完成制作该实例所需的所有工作. 使用专为Cocoa绑定技术设计的现成的NSController对象之一. 也就是说使用
NSObjectController, NSArrayController, NSUserDefaultsController, NSTreeController的实例, 或者使用这些具体NSController子类之一的自定义子类但是如果应用程序非常简单, 并且更放心地编写使用
outlets和target-action实现中介行为所需的粘合代码, 则可以随意使用自定义NSObject子类的实例作为中介控制器. 在自定义NSObject子类中, 还可以使用键值编码、键值观察和编辑器协议实现NSController意义上的中介控制器 -
尽管可以在对象中组合MVC角色, 但最佳的总体策略是保持角色之间的分离. 这种分离增强了对象的可重用性和它们所使用的程序的可扩展性. 如果要合并类中的MVC角色, 则选择该类的主要角色, 然后(出于维护目的)使用同一实现文件中的分类来扩展该类以发挥其他角色
-
设计良好的MVC应用程序的目标应该是使用尽可能多的可复用对象(理论上至少是可重用的). 特别是,视图对象和模型对象应高度可重用. (当然现成的中介控制器对象是可重用的. 特定于应用程序的行为通常尽可能集中在控制器对象中
-
尽管可以让视图直接观察模型以检测状态的变化, 但最好不要这样做. 视图对象应始终通过中介控制器对象来了解模型对象中的更改. 原因有二
- 如果使用绑定机制让视图对象直接观察模型对象的属性, 则可以绕过NSController及其子类为应用程序提供的所有优势:选择和占位符管理以及提交功能并放弃更改
- 如果不使用绑定机制, 则必须对现有视图类进行子类化生成类, 以添加观察模型对象发布的更改通知的能力
-
努力限制应用程序类中的代码依赖性. 类对另一个类的依赖性越大, 重用性就越低. 具体建议因所涉及的两个类的MVC角色而异
- 视图类不应依赖于模型类(尽管在某些自定义视图中这可能是无法避免的)
- 视图类不必依赖于中介控制器类
- 模型类不应依赖于其他模型类以外的任何内容
- 中介控制器类不应依赖于模型类(尽管与视图类似. 如果是自定义控制器类则可能需要这样做).
- 中介控制器类不应依赖于视图类或协调控制器类
- 协调控制器类取决于所有MVC角色类型的类
-
如果Cocoa提供了一个解决编程问题的体系结构, 并且此体系结构将MVC角色分配给特定类型的对象, 请使用该体系结构. 如果这样做, 将更容易将项目放在一起. 例如文档体系结构包括一个Xcode项目模板, 该模板将NSDocument对象(每
nib模型控制器)配置为文件的所有者
Model-View-Controller in Cocoa (OS X)
模型-视图-控制器设计模式是许多可可机制和技术的基础. 因此, 在面向对象设计中使用 MVC 的重要性不仅仅局限于为自己的应用程序实现更高的可重用性和可扩展性. 如果应用程序要合并基于 MVC 的 Cocoa 技术, 则如果应用程序的设计也遵循 MVC 模式, 则该应用程序将最佳工作. 如果应用程序具有良好的 MVC 分离, 则使用这些技术应该相对无痛, 但如果没有良好的分离, 则使用此类技术需要付出更多努力.
OS X 中的 Cocoa 包括以下基于模型视图控制器的体系结构、机制和技术
- 文档体系结构
- 在此体系结构中, 基于文档的应用程序包括整个应用程序的控制器对象(NSDocumentController)、每个文档窗口的控制器对象(NSWindowController)以及组合控制器和模型角色的对象对于每个文档(NSDocument)
- 绑定
- MVC是Cocoa绑定技术的核心. 抽象NSController的具体子类提供现成的控制器对象, 可以对其进行配置, 以在视图对象和正确设计的模型对象之间建立绑定.
- 应用程序可编写脚本能力
- 在设计应用程序使其可编写脚本时, 不仅要遵循MVC设计模式, 还必须正确设计应用程序的模型对象. 通常应将访问应用程序状态和请求应用程序行为的脚本命令发送到模型对象或控制器对象
- Core Data
- Core Data框架管理模型对象的图形, 并通过将这些对象保存到(并从中检索它们)来确保这些对象的持久性. Core Data与Cocoa绑定技术紧密集成. MVC和对象建模设计模式是Core Data体系结构的基本决定因素
- Undo
- 在撤消体系结构中, 模型对象再次发挥核心作用. 模型对象的原始方法(通常是其访问器方法)通常是实现撤消和重做操作的位置. 操作的视图和控制器对象也可能参与这些操作. 例如可能让此类对象为撤消和重做菜单项提供特定标题, 或者可能让它们撤消文本视图中的选择
Object Modeling
本节定义术语, 并介绍特定于Cocoa绑定和Core Data框架的对象建模和键值编码示例. 理解key paths等术语对于有效使用这些技术至关重要. 如果对面向对象设计或键值编码是新知识, 建议阅读此部分.
使用Core Data框架时, 需要一种方法来描述不依赖于视图和控制器的模型对象. 在良好的可重用设计中, 视图和控制器需要一种访问模型属性的方法, 而不强制实施它们之间的依赖关系. Core Data框架通过借用数据库技术的概念和术语来解决这个问题, 具体来说是实体关系模型.
实体关系建模是一种表示对象的方法, 通常用于描述数据源的数据结构, 使这些数据结构映射到面向对象系统中的对象. 请注意实体关系建模并非Cocoa所独有. 这是一个流行的学科, 有一套规则和术语记录在数据库文献中. 它是一种表示形式, 便于存储和检索数据源中的对象. 数据源可以是数据库、文件、Web 服务或任何其他持久存储. 因为它不依赖于任何类型的数据源, 它也可以用来表示任何类型的对象及其与其他对象的关系.
在实体关系模型中, 保存数据的对象称为实体, 实体的组件称为属性, 对其他数据承载对象的引用称为关系. 属性和关系一起称为属性. 通过这三个简单的组件(实体、属性和关系), 可以对任何复杂性的系统进行建模.
Cocoa使用本文档中称为对象建模的传统实体关系建模规则的修改版本. 对象建模在模型-视图-控制器(MVC)设计模式中表示模型对象时特别有用. 这并不奇怪, 因为即使在简单的Cocoa应用程序中, 模型通常是持久性的, 也就是说, 它们存储在数据容器中例如文件
Entities
实体是模型对象. 在MVC设计模式中, 模型对象是应用程序中封装指定数据并提供对该数据进行操作的方法的对象. 它们通常是持久性的, 但更重要的是模型对象不依赖于向用户显示数据的方式.
例如模型对象(对象模型)的结构化集合可用于表示公司的客户群、图书库或计算机网络. 图书馆图书具有属性(如书名、ISBN 编号和版权日期)以及与其他对象的关系, 例如作者和库成员. 理论上, 如果可以识别系统的各个部分, 则系统可以表示为对象模型.
下图显示了员工管理应用程序中使用的示例对象模型. 在此模型中, 部门对部门建模, 员工对员工进行建模
员工管理应用程序对象图

Attributes
属性表示包含数据的结构. 对象的属性可以是简单值, 如标量(例如, 整数、浮点或双精度值), 也可以是C结构(例如字符值数组或NSPoint结构)或基元类的实例(例如、NSNumber、NSData 或Cocoa中的 NSColor). 不可变对象(如NSColor)通常也被视为属性.
在Cocoa中, 属性通常对应于模型的实例变量或访问器方法. 例如员工具有名字、姓氏和工资实例变量. 在员工管理应用程序中, 可以实现表视图来显示Employer对象及其某些属性的集合, 如图所示. 表中的每一行对应于"员工"的实例, 每一列对应于"员工"的属性
员工表视图

Relationships
并非所有模型的属性(properties)都是属性(attributes) — 某些属性是与其他对象的关系. 应用程序通常由多个类建模. 在运行时, 对象模型是组成对象图的相关对象的集合. 这些通常是用户在终止应用程序之前创建并保存到某些数据容器或文件中的持久对象(如在基于文档的应用程序中). 可以在运行时遍历这些模型对象之间的关系以访问相关对象的属性.
例如在员工管理应用程序中, 员工与员工工作的部门之间以及员工与员工经理之间存在关系. 因为经理也是员工, 员工与经理的关系是自反关系(从实体到自身)关系的示例.
关系本质上是双向的, 因此至少在概念上, 部门与部门中工作的员工、员工和员工的直接下属之间也有关系. 下图说明了部门与员工实体之间的关系以及员工反身关系(inverse relationship). 在此示例中部门实体的"员工"关系与员工实体的"部门"关系相反. 但是关系可能只在一个方向上导航, 因此不存在反关系. 例如如果从未对从部门对象中找出员工与其关联的内容感兴趣, 则不必对这种关系进行建模. (请注意虽然一般情况下是这样, 但Core Data可能会对一般Cocoa对象建模施加额外的约束, 而不是将逆建模视为极其高级的选项
员工管理应用程序中的关系

Relationship Cardinality and Ownership 关系基数和所有权
每个关系都有基数. 基数告诉有多少目标对象可以(可能)分解关系. 如果目标对象是单个对象, 则关系称为对一关系. 如果目标中可能有多个对象, 则关系称为对多关系.
关系可以是强制的, 也可以是可选的. 强制关系是需要目标的关系, 例如每个员工都必须与部门关联. 顾名思义可选关系是可选的, 例如不是每个员工都有直接下属. 因此, 下图所示的直接报告关系是可选的.
还可以为基数指定范围. 可选的对一关系的范围为0-1. 员工可能具有任意数量的直接下属, 或指定最小和最大值的范围, 例如0-15, 这也说明了可选的多关系.
下图说明了员工管理应用程序中的基数. 员工对象和部门对象之间的关系是一种强制性的关系, 员工必须属于一个部门, 而只属于一个部门. 部门与其员工对象之间的关系是可选的多关系(由"*"表示). 员工与经理之间的关系是可选的对一关系(以范围0-1表示)—最上层员工没有经理
关系基数

另请注意关系的目标对象有时是拥有的, 有时是共享的.
Accessing Properties
为了使模型、视图和控制器彼此独立, 需要能够以独立于模型实现的方式访问属性. 这是通过使用键值对实现的
Keys
可以使用简单键(通常是字符串)指定模型的属性. 相应的视图或控制器使用键查找相应的属性值. 此设计强制认为属性本身不一定包含数据 — 值可以间接获取或推到.
键值编码用于执行此查找. 它是一种间接访问对象属性的机制, 在某些上下文中自动访问对象的属性. 键值编码的工作原理是使用对象属性的名称(通常是其实例变量或访问器方法)作为访问这些属性值的键.
例如可以使用name键获取部门对象的名称. 如果Department对象具有实例变量或称为name的方法, 则可以返回该键的值(如果没有, 则返回错误). 同样可以使用firstName, lastName, salary键获取员工属性.
Values
给定实体的特定属性的所有值都具有相同的数据类型. 属性的数据类型在其相应的实例变量的声明或其访问器方法的返回值中指定. 例如部门对象name属性的数据类型可能是Objective-C中的NSString对象.
请注意键值编码仅返回对象值. 如果用于为指定键提供值的特定访问器方法或实例变量的返回类型或数据类型不是对象, 则为该值创建NSNumber或NSValue对象并在其位置返回. 如果Department的名称属性为 NSString 类型, 则使用键值编码, 为部门对象的名称键返回的值是 NSString 对象. 如果Department的budget预算属性为float浮动类型, 则使用键值编码, 为Department对象的预算budget键返回的值为NSNumber对象.
同样当使用键值编码设置值时, 如果指定键的相应访问器或实例变量所需的数据类型不是对象, 则使用适当的typeValue方法从传递的对象中提取该值.
对一关系的值只是该关系的目标对象. 例如Employer对象的部门department属性的值是Department对象. 对多关系的值是集合对象. 集合可以是set或array. 如果使用Core Data, 则它是一个set. 否则, 它通常是包含该关系的目标对象的数组). 例如部门对象的employees属性的值是包含Employee对象的集合. 下图显示了员工管理应用程序对象图的示例
员工管理应用程序的对象图

Key Paths
key path是点分隔键的字符串, 用于指定要遍历的对象属性序列. 第一个键的属性由上一个属性确定, 并且每个后续键都相对于上一个属性计算. 关key path允许以独立于模型实现的方式指定相关对象的属性. 使用关key path, 可以指定通过对象图(任何深度)到相关对象的特定属性的路径
键值编码机制实现给定key path类似于键值对的值的查找. 例如在员工管理应用程序中, 可以使用department.name的key path访问Employee对象, 其中部门是员工关系, 名称是部门属性. 如果要显示目标实体的属性, 关key path非常有用. 例如下图中的员工表视图配置为显示员工的部门对象的名称, 而不是部门对象本身. 使用Cocoa绑定, 部门列的值将绑定到显示数组中的员工对象department.name
显示部门名称的员工表视图

并不是关key path中的每个关系都必然具有值. 例如如果员工是CEO, 则manager关系可以为nil. 在这种情况下, 键值编码机制不会中断, 它只是停止遍历路径并返回适当的值例如nil
理解如有错误 望指正 转载请说明出处