《结构型设计模式》之iOS系统框架实践

结构型设计模式是从程序的结构上解决模块之间的耦合问题,主要包括适配器模式、桥接模式、组合模式、装饰器模式、外观模式、享元模式和代理模式等7种经典设计模式,在iOS系统框架中组合模式、装饰器模式和享元模式是有经典实现的,而适配器模式、桥接模式、外观模式和代理模式iOS系统框架中的实现并不明显,但在第三方框架或者贝聊(我所在的公司)的App是有用到的,为便于讲解,本文会挑选最恰当的例子。

本文是设计模式之iOS系统框架实践系列中的第二篇(总共三篇),如果您对《创建型设计模式》感兴趣,建议看看我的前一篇文章《创建型设计模式》之iOS系统框架实践

适配器模式(Adapter)

适配器模式是将一种接口,转换成另外一种接口,一般被适配的类的功能与外界所希望的一致,只是接口与外界所希望的不同,所以需要适配接口。一般需要新旧接口的转换时用到。

不少资料上讲iOS中的delegate委托就是一种适配器模式,个人觉得太牵强。虽然将一个现有的类,扩展遵循指定delegate的协议,实现了接口的改变,但功能完全是新添加的,何来接口转换只说?(原来苹果官方资料说protocol就是适配器模式,也是醉了)

由于暂未发现iOS系统框架中特别典型的适配器模式,本文就以贝聊App中的IM(instant message即时通讯)项目为例。

贝聊IM用的是阿里百川云望(下文简称云望)服务器,UI自己实现。对于每一条消息,云望都有自己的消息类型YWMessage(云望SDK中定义的一个协议),大体协议结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import UIKit
// 消息体实体类
class YWMessageBody {

}

// 消息协议
protocol YWMessage {
var messageId: String { get }
var fromPersonId: String { get }
var toPersonId: String { get }
var createDate: TimeInterval { get }
var conversationId: String { get }
var messageBody: YWMessageBody { get }
}

为解除第三方SDK与贝聊App内部聊天UI过渡耦合,需要将云望消息类型转换成贝聊自己的消息类型,也是个协议,贝聊消息协议定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
protocol MessageBody {

}

protocol Message {
var messageID: String { get }
var senderID: String { get }
var receiverID: String { get }
var createTimeInterval: TimeInterval { get }
var conversationID: String { get }
var messageBody: MessageBody { get }

}

云望消息类型YWMessage和贝聊消息类型Message接口功能基本一致,只是接口定义不一致,这就需要接口的适配。
虽然云望SDK消息类型公开的接口是个YWMessage协议,但当有消息发送过来时,所收到的是一个实现了YWMessage协议的具体私有类,YWMessageConcrete,为实现接口转换,定义一个做消息转换的适配器消息类MessageAdaperForYWMessage:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class MessageAdaperForYWMessage: Message {
let ywMessage: YWMessage
init(ywMessage: YWMessage) {
self.ywMessage = ywMessage
}
var messageID: String {
return ywMessage.messageId
}
var senderID: String {
return ywMessage.fromPersonId
}
var receiverID: String {
return ywMessage.toPersonId
}
var createTimeInterval: TimeInterval {
return ywMessage.createDate
}
var conversationID: String {
return ywMessage.conversationId
}
var messageBody: MessageBody {
return ywMessage.messageBody
}
}

extension YWMessageBody: MessageBody {

}

组合模式(Composite)

组合模式就是将对象组合成树形结构,以表示“部分-整体”的层次结构,而且单个对象和组合对象的接口一致,如果从整个树形结构中截取任何一部分,client不知是单个对象,还是组合对象。对应的类图如下:

容器类(Composite)和叶子类(Leaf)其实都是实现Component接口的子类,client在使用时不做区分,都当作Component使用。容器类可以添加容器或者叶子做子类,而叶子类不能添加任何子类。所形成的树形结构如下:

在UIKit中,我们熟悉的不能再熟悉的UIView和CALayer是经典的组合模式,并且UIView和CALayer并没有区分容器类和叶子类,而是统一只有一个单一容器类UIView和CALayer。其实这样使用起来更加亲民,不用区分是容器还是叶子。

UIView的实现起来是相当简单的(为不与系统UIView混淆,这里取View),如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import UIKit

class View: Equatable {
var subviews: [View] = []
var superView: View?

func addSubview(subview: View) {
subviews.append(subview)
}

func removeSubview(subview: View) {
if let index = subviews.index(of: subview) {
subviews.remove(at: index)
}
}

func draw(_ rect: CGRect) {
// 具体的绘制图形的代码
}
}

// Array.index(of:)必须遵循Equatable协议
func == (lhs: View, rhs: View) -> Bool {
return lhs === rhs
}

具体使用时,先创建各个View,然后根据需要添加层级关系如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let view1 = View()
let view2 = View()
let view3 = View()
let view4 = View()
let view5 = View()
let view6 = View()
let view7 = View()
let view8 = View()

view1.addSubview(subview: view2)
view1.addSubview(subview: view3)
view3.addSubview(subview: view4)
view3.addSubview(subview: view5)
view4.addSubview(subview: view6)
view6.addSubview(subview: view7)
view6.addSubview(subview: view8)

形成的树形结构如下图:

这也是我们常说的UIView层级。

桥接模式(Bridge)

桥接模式是把抽象部分和实现部分分离,使它们都可以独立的变化。

由于暂未发现iOS系统框架中特别典型的适配器模式,而在贝聊APP和第三方库我也没遇到过,本文就以Design-Patterns-In-Swift的例子为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
protocol Switch {
var appliance: Appliance {get set}
func turnOn()
}

protocol Appliance {
func run()
}

class RemoteControl: Switch {
var appliance: Appliance

func turnOn() {
self.appliance.run()
}

init(appliance: Appliance) {
self.appliance = appliance
}
}

class TV: Appliance {
func run() {
print("tv turned on");
}
}

class VacuumCleaner: Appliance {
func run() {
print("vacuum cleaner turned on")
}
}

用法:

1
2
3
4
5
var tvRemoteControl = RemoteControl(appliance: TV())
tvRemoteControl.turnOn()

var fancyVacuumCleanerRemoteControl = RemoteControl(appliance: VacuumCleaner())
fancyVacuumCleanerRemoteControl.turnOn()

装饰器模式(Decorator)

装饰器模式,也叫装饰者模式,能够实现动态的为对象添加功能,是从一个对象外部来给对象添加功能。装饰器模式就是基于对象组合的方式,可以很灵活的给对象添加所需要的功能。

根据具体实现方式的不同,分类装饰器和对象装饰器两种模式。

类装饰器模式,就是直接扩展现有的类,不新增一个类。swift中的extension特性对应的就是类装饰器模式。比如想给UIImage增加一个ratio的属性,直接extenstion UIImage类即可:

1
2
3
4
5
6
7
import UIKit

extension UIImage {
var ratio: CGFloat {
return size.height / size.width
}
}

使用起来也很方便:

1
2
let image = UIImage(named: "sampleImage")!
print(image.ratio)

对象装饰器模式,是新增一个装饰类,然后持有一个被装饰类,通过给装饰类添加功能,增加被装饰类的功能。在UIKit中,UIView与CALayer,UIViewController与UIView是典型的对象装饰器模式。

UIView装饰了CALayer,给CALayer添加了事件响应等全新的功能。UIViewController装饰了UIView,给UIView添加了viewDidLoad、viewDidAppear 、viewWillAppear、viewWillDisappear 、viewDidDisappear、viewDidLayoutSubviews等生命周期,UIViewController带UIView来到了全新的世界的同时,也把自己演变成身份十分独特的控制器类,进而成就了经典的MVC设计模式。

比如UIView装饰CALayer的简易实现(为不与系统UIView混淆,这里取View)如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class View: UIResponder, CALayerDelegate {
// 被装饰的layer
lazy var layer: CALayer = {
let layer = CALayer()
// 注意layer的delegate是View本身
layer.delegate = self
return layer
}()

// 需要将CALayer功能暴露给外界的接口
var frame: CGRect {
set {
layer.frame = newValue
}
get {
return layer.frame
}
}
// 给CALayer新增加的事件响应功能
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {

}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {

}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {

}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {

}
}

可以定义一些接口,将被装饰的类的功能暴露给外界。

外观模式(Facade)

外观模式为子系统中的一组接口提供一个统一的更高层次的接口,使得子系统更加容易使用,说白了就是为了简化复杂的接口。

iOS系统框架中特别典型的外观模式暂未发现,其实我们常用的SDWebImage和YYWebImage在API设计上都采用了典型的外观模式。

例如想从一个URL加载图片:

1
2
3
4
5
6
import UIKit
import SDWebImage

let imageView = UIImageView()
imageView.sd_setImage(with: URL(string: "http://www.domain.com/path/to/image.jpg"),
placeholderImage: UIImage(named: "placeholder.png"))

看似把URL直接传入UIImageView的sd_setImage(with:placeholderImage:)方法这么简单,其实从服务器加载图片其实是一个比较复杂的过程,主要涉及三个模块:

  1. 缓存管理模块,查看有没有对应的缓存,以及缓存一张图片到磁盘或者内存
  2. 图片下载模块
  3. 网络请求模块,图片下载模块又依赖网络请求模块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import UIKit
extension UIImageView {
func setImage(with url: URL, placeHolder: UIImage) {
let imageCacher = ImageCacher.shared
let imageDownloader = ImageDownloader.shared

if let cachedImage = imageCacher.cachedImageFor(url: url) {
image = cachedImage
return
}

image = placeholder
imageDownloader.downloadImageFrom(url: url) { (image, error) in
self.image = image
if let image = image {
imageCacher.cache(image: image, url: url)
}
}
}
}

其中依赖了缓存管理模块和图片下载模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 图片下载模块
class ImageDownloader {
static let shared = ImageDownloader()

func downloadImageFrom(url: URL, completionHandler:@escaping (UIImage?, Error?) -> Void) {
// 用到了网络下载模块
let downloadTask = URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
DispatchQueue.main.async {
if let imageData = data {
let image = UIImage(data: imageData)
completionHandler(image, nil)
} else {
completionHandler(nil, error)
}
}
})
downloadTask.resume()
}
}

// 图片缓存模块
class ImageCacher {
static let shared = ImageCacher()

// 从缓存中取缓存的UIImage,没有就返回nil
func cachedImageFor(url: URL) -> UIImage? {
return nil
}

// 缓存图片
func cache(image: UIImage, url: URL) {
// 缓存逻辑
}
}

使用起来跟SDWebImage一般无二:

1
2
3
let imageView = UIImageView()
imageView.setImage(with: URL(string: "http://www.domain.com/path/to/image.jpg"),
placeholder: UIImage(named: "placeholder.png"))

享元模式(Flyweight)

享元模式就是实现类的复用,因为当相同的类创建很多,会消耗很大系统内存。在UIKit中,UITableViewCell和UICollectionViewCell的复用就是典型的享元模式,如果UITableViewCell和UICollectionViewCell不复用,每一个cell都需要创建新的实例,不但消耗内存,同时也消耗时间,进而导致卡顿掉帧。

我们在使用UITableViewCell时,一般分为四步:

  1. 定义一个自定义cell
  2. 创建tableview
  3. 注册cell
  4. 从cell池(由tableView维护)取cell
1
2
3
4
5
6
7
8
9
import UIKit
// 1. 定义一个自定义cell
class CustomCell: UITableViewCell { }
// 2. 创建tableview
let tableView = UITableView()
// 3. 注册cell
tableView.register(CustomCell.self, forCellReuseIdentifier: String(describing: CustomCell.self))
// 4. 从cell池取cell
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: CustomCell.self))

UITableViewCell复用逻辑的简易实现(为不与系统UITableView混淆,这里取TableView),如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class TableView: UIScrollView {
// 注册的cell class
var registerredCellClasses: [String: UIKit.UITableViewCell.Type] = [:]
// cell池
var cellPool: [String: [UITableViewCell]] = [:]

// 注册复用的cell
func register(_ cellClass: UIKit.UITableViewCell.Type, forCellReuseIdentifier identifier: String) {
registerredCellClasses[identifier] = cellClass
}

// 复用cell方法
func dequeueReusableCell(withIdentifier identifier: String) -> UITableViewCell? {
// 检查有没注册
guard let cellClass = registerredCellClasses[identifier] else { return nil }
if var cells = cellPool[identifier],
cells.count > 0 {
// 检查cell池中有没有可复用的cell
let cell = cells.removeFirst()
cellPool[identifier] = cells

// 准备复用
cell.prepareForReuse()

return cell
} else {
// cell池没有可复用的cell,新创建一个
let cell = cellClass.init(style: .default, reuseIdentifier: identifier)
return cell
}
}

// 回收超出屏幕可视区域的cell,
// TableView会监测正在显示cell的状态,当有cell完全进入不可视区域时,进行回收,逻辑比较复杂,这里不实现了
func collectCellInvisible(invisibleCell: UITableViewCell) {
guard let identifier = invisibleCell.reuseIdentifier else { return }
cellPool[identifier]?.append(invisibleCell)
}
}

实现中,

  1. 需要记录注册cell和维护一个cell池。
  2. 当调用dequeueReusableCell(withIdentifier:)方法时,会先判断对应idenfitier有没注册的cell,确定有注册后,然后再看现有的cell池有没有可以复用的,有就复用,没有就直接创建一个新的cell。
  3. TableView需要监测正在显示cell的状态,当有cell完全进入不可视区域时,进行回收。

代理模式(Proxy)

代理模式是为其他对象提供一种代理以控制这个对象的访问,解决直接访问某些对象时出现的问题。

Foundation中的NSProxy采用的就是代理模式,但我们日常开发并不常用,本文就以Design-Patterns-In-Swift的例子为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import UIKit
// 专门负责开门的操作人员,不负责身份校验
protocol DoorOperator {
func open() -> String
}

// 中南海开门操作人员
class SeaPalaceDoorOperater : DoorOperator {
func open() -> String {
return "中南海欢迎您!"
}
}

// 中南海开门操作人员的代理,除了开门职责外,还负责校验身份
class SeaPalaceDoorOperaterProxy : DoorOperator {
private var doorOperator: SeaPalaceDoorOperater!

func authenticate(password: String) -> Bool {
guard password == "pass" else {
return false
}

doorOperator = SeaPalaceDoorOperater()

return true
}

func open() -> String {
guard doorOperator != nil else {
return "对不起,您没有开门权限"
}

return doorOperator.open()
}
}

其中开门的操作人员DoorOperator只有开门这一单一职责,但开门是需要校验的,所以需要一个DoorOperator的代理,让其承担一部分校验的职责,具体开门的还是由被代理的DoorOperator来。这其实跟装饰者模式有点像,只是代理模式更关注对被代理类的控制,就像SeaPalaceDoorOperaterProxy主要控制外界对被其代理对象SeaPalaceDoorOperater的访问。

具体用法如下:

1
2
3
4
5
let seaPalaceDoorOperater = SeaPalaceDoorOperaterProxy()
seaPalaceDoorOperater.open()

seaPalaceDoorOperater.authenticate(password: "pass")
seaPalaceDoorOperater.open()

总结

在iOS系统框架中,

  1. UIView和CALayer是典型的组合模式。
  2. swift中的extension是典型的类装饰器模式。
  3. UIView与CALayer,UIViewController与UIView是典型的对象装饰器模式。
  4. UITableViewCell与UICollectionViewCell的复用机制是典型的享元模式。
  5. 苹果官方资料说protocol是变相的适配器模式,我还能说什么。

在贝聊App的项目中,云望消息类型转换到贝聊消息类型是典型的适配器模式。

在第三方框架SDWebImage中,UIImageView的sd_setImage(with:placeholderImage:)等下载网络图片的便捷方法是典型的外观模式。

而桥接模式和代理模式,由于未找到合适的例子,采用了Design-Patterns-In-Swift中的例子。

参考文章

  1. iOS Design Patterns
  2. Design-Patterns-In-Swift
  3. 源码分析之SDWebImage
  4. Cocoa Design Patterns
分享到 评论