iOS 设计模式漫谈

一、当我们谈论设计模式的时候,我们在谈什么?

设计模式是为特定场景下的问题而定制的解决方案,是对特定面向对象设计问题主要方面的一种抽象。程序如果在设计中使用了设计模式,将来就更易于复用和扩展、且易于变更。另外,基于设计模式的程序会更加简洁和高效,因为达成同样目的所需的代码会更少。

最早对设计模式做出权威性论述和分类的是 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 中很常见,一般有两种:

  1. view 和 controller 沟通的方式

    view 的编程方式一般是 generic 的,因此怎么显示 view 或得到显示 view 需要的数据源一般通过询问它的 delegate 完成。通常情况下会将 controller 作为 view 的 delegate(或 data source),例如:

    • UITableView
    • UITextField
    • UIGestureRecognizer
  2. 其他对象之间的交互

    可以用于任何「不关心对方是什么类型,只要可以作为我的 delegate 能帮我完成一些特定任务」的场合。

这些「特定任务」通过协议来规定,因此 Delegation 是基于协议的。

虽然 Delegation 不是 GoF 提出的一种,但也间接地实现了一些经典的设计模式,例如 Adapter,其含义为将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起工作。类图如下

典型的 Adapter 模式是会对 Adaptee 类构造一个 wrapper 类也就是 Adapter,并在 Adapter 中实现客户希望的另一个接口。Delegation 没有对类进行封装,但是间接实现了让接口不兼容的类一起工作的目的。

其他用 Delegation 实现的设计模式将在下文提到。

Observer

Observer 模式定义了对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。本质上是一种「发布-订阅」模型,使得对象和它的观察者之间解耦,可以在不知道彼此的情况下交流。其类图如下

在 Cocoa Touch 中有两种实现方式:

  1. Notification

    Notification 机制实现了一对多的消息广播。对象可以向 notification center 注册成为某具体通知的 observer,这样当有其他对象发布了该通知时,notification center 就会通知 observer 对象。

    具体通知例如 UIKeyboardWillShow(键盘将弹出,这时 UI 可以相应做出变化),UIApplicationDidEnterBackground(app 进入后台,可以迅速保存一些当前数据)等等系统通知,或者一些自定义通知。

  2. 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 中的应用有两种情况:

  1. 单一资源

    例如 CoreLocation 中的 CLLocationManager 类,定义了对 GPS 设备所提供服务的单一访问点。又如 UIScreen.main 等。

  2. 统一管理

    例如 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 框架中,NSNumberNSStringNSDataNSDictionaryNSSetNSArray 以及它们的 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 中应用了这一设计原则的类例如 UIScrollViewUIDatePicker,它们是复合视图,结合了其他类的简单视图对象并协调其交互。

另外,基于这一设计模式,有两种典型的实现方式:

  1. Category

    它是Objective-C语言的特性,可以动态向类或结构添加额外的方法,而无需子类化。运行时,通过这种方式添加的方法与类中定义的原始方法没有任何差别。因此这种方式添加的方法也会被子类继承。尤其好的一点是,它可以用于看不到源码的类。需要注意的是:

    • 不能向类添加实例变量
    • 如果添加的方法覆盖了类中已有的方法,运行时行为将不可预测

    Category 并没有严格地实现装饰模式,没有包装类的对象,也没有动态扩展类而是一种编译时的特性。它通过其他方式实现了装饰模式的目的。

  2. 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,也有 UINavigationControllerUITabBarController 来作为其中介者。

Memento

备忘录模式用于保存数据和恢复数据。在不破坏封装的前提下捕获一个对象的内部状态,并在该对象之外保存这个状态,方便以后将该对象恢复到原先保存的状态。类图如下

在 Cocoa Touch 中的应用包括:

  • State Restoration

    当 app 退出时,可以通过保存当前 state 以在下次打开该 app 时恢复到退出前的状态。

  • Archiving

    归档操作将对象类型及其属性保存为一个归档文件,可以保存在文件系统中或进行网络传输。通常需要对 MVC 中的 model 进行 encode 归档,再从该归档 decode 读取 model。 可以分别利用 NSKeyedArchiverNSKeyedUnarchiver,并遵从 NSCoding 协议。

参考文献

最后,附上参考文献:

  1. 官方文档 Cocoa Design Patterns
  2. 设计模式的书(本文截图出处)Pro Objective-C Design Patterns for iOS
  3. 开源项目 Design-Patterns-In-Swift
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容

  • 37.cocoa内存管理规则 1)当你使用new,alloc或copy方法创建一个对象时,该对象的保留计数器值为1...
    如风家的秘密阅读 829评论 0 4
  • 2015/04/22更新:最新更新到Xcode6.3和Swfit 1.2。 更新日志:这个教程由Vicent Ng...
    木易林1阅读 277评论 0 0
  • 转:http://www.cocoachina.com/programmer/20151019/13746.htm...
    Style_伟阅读 1,293评论 0 3
  • 之前看了很多面试题,感觉要不是不够就是过于冗余,于是我将网上的一些面试题进行了删减和重排,现在分享给大家。(题...
    Job_Yang阅读 12,100评论 12 143
  • 注:解读只是自己暂时的浅见。 【解读】 1、老子发问,恭维与呵斥、行善与作恶相差多少呢?常理觉得差别比较大,但是老...
    吾宗老孙子阅读 527评论 0 0