一、当我们谈论设计模式的时候,我们在谈什么?
设计模式是为特定场景下的问题而定制的解决方案,是对特定面向对象设计问题主要方面的一种抽象。程序如果在设计中使用了设计模式,将来就更易于复用和扩展、且易于变更。另外,基于设计模式的程序会更加简洁和高效,因为达成同样目的所需的代码会更少。
最早对设计模式做出权威性论述和分类的是 Design Patterns: Elements of Reusable Object-Oriented Software,由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著,简称 Gang of Four (GoF)。书中论述了根据创建型(Creational)、结构型(Structural)、行为型(Behavioral)划分的一共 23 种设计模式。
接下来会结合在 Cocoa Touch 中的应用来谈论一些常见的设计模式。
二、MVC——设计模式之王
MVC 几乎是构建 Cocoa 的基石,也是最广泛应用的设计模式。它把对象根据其在应用程序中的作用划分为三类并鼓励依此划分代码区域:
- model:代表数据结构
- view:用来可视化数据结构,或负责与用户的交互
- controller:协调 model 和 view,从 model 拿到数据并用 view 进行显示,或者收听通知并处理数据等
MVC 其实并不是一种单一的设计模式,而是许多设计模式的组合。例如,controller 可以视为 controller 和 view 的 Mediator 并应用了 Strategy,view 的结构是 Composite,view 和 controller 的 target-action 模式应用了 Command,controller 通过 Observer 接收 model 发生变化的通知。
三、Cocoa Touch 中最常见的两种模式
Delegation
Delegation 并不属于 GoF 提出的 23 种设计模式中的一种,但是在 Cocoa Touch 中很常见,一般有两种:
-
view 和 controller 沟通的方式
view 的编程方式一般是 generic 的,因此怎么显示 view 或得到显示 view 需要的数据源一般通过询问它的 delegate 完成。通常情况下会将 controller 作为 view 的 delegate(或 data source),例如:
UITableView
UITextField
UIGestureRecognizer
-
其他对象之间的交互
可以用于任何「不关心对方是什么类型,只要可以作为我的 delegate 能帮我完成一些特定任务」的场合。
这些「特定任务」通过协议来规定,因此 Delegation 是基于协议的。
虽然 Delegation 不是 GoF 提出的一种,但也间接地实现了一些经典的设计模式,例如 Adapter,其含义为将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起工作。类图如下
典型的 Adapter 模式是会对 Adaptee
类构造一个 wrapper 类也就是 Adapter
,并在 Adapter
中实现客户希望的另一个接口。Delegation 没有对类进行封装,但是间接实现了让接口不兼容的类一起工作的目的。
其他用 Delegation 实现的设计模式将在下文提到。
Observer
Observer 模式定义了对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。本质上是一种「发布-订阅」模型,使得对象和它的观察者之间解耦,可以在不知道彼此的情况下交流。其类图如下
在 Cocoa Touch 中有两种实现方式:
-
Notification
Notification 机制实现了一对多的消息广播。对象可以向 notification center 注册成为某具体通知的 observer,这样当有其他对象发布了该通知时,notification center 就会通知 observer 对象。
具体通知例如
UIKeyboardWillShow
(键盘将弹出,这时 UI 可以相应做出变化),UIApplicationDidEnterBackground
(app 进入后台,可以迅速保存一些当前数据)等等系统通知,或者一些自定义通知。 -
Key-Value Observing (KVO)
KVO 主要用来观察其他对象属性的变化。尤其在 MVC 模式中,可以使 view 和 model 的变化保持同步(通过 controller)。KVO 和 Notification 的区别在于它不需要 notification center,而是直接将变化告知了 observer。Notification 更多地应用于系统通知。
四、经典设计模式在 iOS 中的应用
Creational
Prototype
使用原型实例指定创建对象的种类,并通过复制这个原型创建新的对象。
这种模式很简单,指无需手动创建、通过复制就可以制造同一类型的多个实例。
在 Cocoa Touch 中的一个典型应用是 UITableViewCell
:
Use dynamic prototypes to design one cell and then use it as the template for other cells in the table. Use a dynamic prototype when multiple cells in a table should use the same layout to display information.
要通过原型复制得到新的对象,需要实现深复制协议。对于 NSObject
的子类,需要实现 NSCopying
协议及其方法 - (id)copyWithZone:(NSZone *)zone
。通过调用实例方法 -(id)copy
进行复制,该方法默认会调用 [self copyWithZone:nil]
。
Singleton
单例模式保证一个类仅有一个实例,并提供一个访问它的全局访问点。
在 Cocoa Touch 中的应用有两种情况:
-
单一资源
例如 CoreLocation 中的
CLLocationManager
类,定义了对 GPS 设备所提供服务的单一访问点。又如UIScreen.main
等。 -
统一管理
例如
FileManager.default
、或常见的 MVC-N 模式中的 N(NetworkManager)等。
典型的创建单例的代码如下:
这里,sharedInstance
是 type property,static
保证了它只初始化一次且线程安全。同时将 init
方法声明为 private
保证了不能在类外创建该类的对象。
Abstract Factory
抽象工厂模式提供了用于创建一系列相互关联的对象的接口,而无须指定它们具体的类。将客户端和具体类型解耦。类图如下
在 Cocoa Touch 中有个很重要的应用就是 Class Cluster(类簇)。它封装了一系列私有具体子类,提供了一个公有抽象超类的接口。抽象超类声明了一系列创建私有子类对象的方法,再根据不同的方法动态选择合适的具体子类类型。
Class clusters in Cocoa can generate only objects whose storage of data can vary depending on circumstances.
在 Foundation 框架中,NSNumber
、NSString
、NSData
、NSDictionary
、NSSet
和 NSArray
以及它们的 mutable 版本都是基于类簇实现的。
拿 NSNumber
举例来说,该抽象超类声明了一系列创建对象的方法:
不同方法返回的对象可能属于不同的私有子类,其具体类型对调用者不可见。
With class clusters there is a trade-off between simplicity and extensibility.
类簇简化了类的接口,使得学习和使用类变得更容易。然而,自定义抽象超类的子类也变得复杂。
Structural
Decorator
用于动态地给对象增加一些额外的职责。这种方式比生成子类的实现更为灵活。类图如下
装饰通过包装类的对象来扩展其行为,诠释了设计原则:
Classes should be open to extension but closed to modification.
在 Cocoa Touch 中应用了这一设计原则的类例如 UIScrollView
和 UIDatePicker
,它们是复合视图,结合了其他类的简单视图对象并协调其交互。
另外,基于这一设计模式,有两种典型的实现方式:
-
Category
它是Objective-C语言的特性,可以动态向类或结构添加额外的方法,而无需子类化。运行时,通过这种方式添加的方法与类中定义的原始方法没有任何差别。因此这种方式添加的方法也会被子类继承。尤其好的一点是,它可以用于看不到源码的类。需要注意的是:
- 不能向类添加实例变量
- 如果添加的方法覆盖了类中已有的方法,运行时行为将不可预测
Category 并没有严格地实现装饰模式,没有包装类的对象,也没有动态扩展类而是一种编译时的特性。它通过其他方式实现了装饰模式的目的。
-
Delegation
前文有介绍过,同样地,它也不是装饰模式的严格实现,没有对类的对象进行包装,而是让类将一些指定行为动态交给类的 delegate 去完成。除了 delegation 定义的方法,没有其他共享的接口。
Composite
组合模式将对象组合成树形结构来表示「部分-整体」的层次结构,统一了访问单个对象和组合对象的接口。
在 Cocoa Touch 中典型的应用就是 View Hierarchy:
view 可以添加其他 view 作为 subview,同时这些 subview 又可以作为其他 view 的 superview。每个 view 都只有一个 superview,可以有任意数量的 subview。这样就形成了树状结构。这种结构的好处是方便遍历:
-
drawing
当 window 需要显示时,superview 会在 subview 之前进行 render。当有消息发送给 view,消息会进一步发送给 subview。这样 view hierarchy 就可以当做一个整体的 view 来对待。
-
event handling
用于形成 responder chain 来处理各种事件,详见 Chain of Responsibility 模式。
Proxy
用于控制对其他对象的访问。
Cocoa Touch 中有个 NSProxy
类,用来作其他对象的代理。有多种用途:
-
lazy instantiation
proxy 对象作为占位符,实现昂贵资源的懒加载。例如 Mail app 中,较大的附件资源通常先显示为占位符,当用户点击之后再去真正地下载附件。
-
method forwarding
proxy 对象将消息转发给其代理的对象,可以用这种机制类似地实现多重继承。
-
sentry objects for security
为了保证安全性,先由 proxy 对象检查访问权限。
Facade
外观模式为子系统中的一组接口提供一个统一的高层接口,让子系统更易于使用。
在 Cocoa Touch 中的一个例子是 UIImage
,它提供了一个统一的接口来加载图片,将图片格式的细节隐藏起来。
Behavioral
Chain of Responsibility
责任链模式将多个对象连成一条链,并沿着这条链传递请求,直到有一个对象响应为止。使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间发生耦合。
在 Cocoa Touch 中的典型应用是 responder chain(响应链)。下图是含 label、button 等的例子及其默认的响应链:
当 app 接收了某种 event,UIKit 会自动将该 event 传给 first responder。如果它不能处理 event,会把 event 发送给其 next responder。未处理的 event 会沿着响应链传递,直到某个 responder 可以处理为止。
细节可以参考官方文档 Understanding Event Handling, Responders, and the Responder Chain。
Command
命令模式将一个请求封装为一个对象,对请求排队或者记录请求日志,支持可撤销的操作。它将提出请求的对象与接受并执行请求的对象解耦。类图如下
用 NSInvocation
类来封装 Objective-C 的消息(也包括目标对象和参数等)。在 Cocoa Touch 中有两个应用:
-
Target-Action 机制
在这种机制下,一个 control 对象(例如 button、slider 或 tex field 等)会向其 target 发送封装好的 action 消息。target 接收后将对消息进行处理。
-
NSUndoManager
类用于撤销操作和恢复操作。它管理了一个撤销栈和恢复栈,当命令执行后,就将其 push 到撤销栈。如果需要撤销,则从撤销栈中 pop 一个命令,并 push 到恢复栈。如果需要恢复,就从恢复栈中 pop 一个命令,再 push 到撤销栈。两个栈配合使用,可以方便地执行撤销和恢复操作。
Mediator
用一个对象(中介者)来封装一系列对象的交互方式,使各对象不需要显式地相互引用从而解耦,且可以独立地改变它们之间的交互。
典型应用就是 MVC 模式中的 controller。在 Cocoa Touch 中,负责这一角色的是 UIViewController
,来协调 model 和 view 的交互。 同样地,对于多个 view controller,也有 UINavigationController
和 UITabBarController
来作为其中介者。
Memento
备忘录模式用于保存数据和恢复数据。在不破坏封装的前提下捕获一个对象的内部状态,并在该对象之外保存这个状态,方便以后将该对象恢复到原先保存的状态。类图如下
在 Cocoa Touch 中的应用包括:
-
State Restoration
当 app 退出时,可以通过保存当前 state 以在下次打开该 app 时恢复到退出前的状态。
-
Archiving
归档操作将对象类型及其属性保存为一个归档文件,可以保存在文件系统中或进行网络传输。通常需要对 MVC 中的 model 进行 encode 归档,再从该归档 decode 读取 model。 可以分别利用
NSKeyedArchiver
和NSKeyedUnarchiver
,并遵从NSCoding
协议。
参考文献
最后,附上参考文献:
- 官方文档 Cocoa Design Patterns
- 设计模式的书(本文截图出处)Pro Objective-C Design Patterns for iOS
- 开源项目 Design-Patterns-In-Swift