心辰·Dev

iOS 开发常见设计模式解析 | 结构型模式

适配器

定义

将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

类图

对象适配器
类适配器
目标(Target)定义了客户可以使用的与特定领域相关的接口。被适配者(Adaptee)定义着已经存在的待适配的接口,适配器(Adapter)对被适配者的接口和目标接口进行适配。适配器使用多重继承进行适配的是类适配器,而依赖于对象组合进行适配的是对象适配器。

实践

Objc 语言本身不支持多重继承,但它支持灵活地定义协议(protocol),类似于 Java 语言中的接口(interface),使得一系列方法声明可以独立于类继承之外,通过遵从协议同样可以形成多重继承,实现对象适配器。虽然不符合适配器模式的结构描述,但其思想仍然是使原本接口不匹配的类可以协同工作。
在 iOS 编程中一个对象适配器的实现需要借助于协议(protocol)。客户想要与接口不兼容的类进行消息传递,可以先定义一个协议(protocol),使适配器类(Adapter)遵从协议,实现一个或多个协议中的方法,这样客户就可以使用协议中的接口,通过适配器类向其中引用的不兼容类(Adaptee)转发消息。协议(protocol)常用于iOS 开发中的委托(delegate)模式。 Block 同样可以实现适配器模式,用 Block 替代协议进行信息的传递。

效果

  • 使用一个可复用的类,该类与其他不相关或不可预见的类协同工作。
  • 适配的目标(Target)与被适配者(Adaptee)接口越不相似,需要适配器做的工作越多。
  • 继承两个目标(Target)可以实现双向适配器,提供透明操作。

桥接

定义

将抽象部分与它的实现部分分离,使它们都可以独立地变化。

类图

桥接
将抽象和它的实现部分分别放在独立的类层次结构中。抽象接口(Abstraction)维护一个指向实现对象(Implementor)的指针。实现类(Implementor)接口提供基本操作,而抽象(Abstraction)定义更高层次的操作。

实践

iOS 开发中常用的类大多数是底层类的抽象层,它们直接使用 Core Foundation 及其相关框架中暴露的 C API 来完成更高层次的操作,底层具体复杂的实现对 iOS 开发者大多数情况下是透明的。
在使用第三方框架时,不同框架对相同功能的实现可能有所不同,如果对框架的接口直接使用,后期想切换至另一套类似的框架,将造成业务代码多处改动。如果在三方库之上再抽象一层,即抽象出一套固定的接口(Abstraction),在抽象层引用实现类并使用实现类的 API, 当要切换至另一套类似框架时,只需更改抽象层的实现,而不用改动上层业务代码。如以下接口实现了对 AFNetworking 框架 HTTP 请求功能的抽象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (NSURLSessionDataTask *)dataTaskWithHTTPMethod:(NSString *)method
requestSerializer:(AFHTTPRequestSerializer *)requestSerializer
URLString:(NSString *)URLString
parameters:(id)parameters
constructingBodyWithBlock:(nullable void (^)(id <AFMultipartFormData> formData))block
error:(NSError * _Nullable __autoreleasing *)error {
NSMutableURLRequest *request = nil;

if (block) {
request = [requestSerializer multipartFormRequestWithMethod:method URLString:URLString parameters:parameters constructingBodyWithBlock:block error:error];
} else {
request = [requestSerializer requestWithMethod:method URLString:URLString parameters:parameters error:error];
}

__block NSURLSessionDataTask *dataTask = nil;
dataTask = [_manager dataTaskWithRequest:request
completionHandler:^(NSURLResponse * __unused response, id responseObject, NSError *_error) {
[self handleRequestResult:dataTask responseObject:responseObject error:_error];
}];

return dataTask;
}

效果

  • 抽象和实现没有固定的绑定关系,运行时实现部分可被切换。
  • 对一个抽象的实现部分的修改不对客户产生影响。
  • 在多个对象间共享实现,同时实现细节对客户透明。

组合

定义

将对象组合成树形结构以表示“部分-整体”的层次结构。使得用户对单个对象和组合对象的使用具有一致性。

类图

组合
组合体(Component)为组合中的对象声明接口,为子组件声明尽可能多的公共操作,并声明接口用于访问子组件。可分解体(Composite)表示有子组件的组件,定义自己的行为并存储子组件,实现与子组件有关的操作,叶子体(Leaf)表示在组合中没有子节点的对象。

实践

组合模式会形成多个类的树形结构,树最顶端的节点(类)对外暴露接口,其下的子节点,即组合体内的类的实现是对外隐藏的。以 Cocoa Touch 框架中的 UITableViewCell 举例,一个 Cell 并不是最基本的视图单元,Cell 中包含了多个 UILabel / UIImageView / UIView 视图,基于 UITableViewCell 的开发者不需要知道这些视图如何在内部进行组合,只需要直接使用 UITableViewCell 的接口,并且可以通过 textLabel / contentView / backgroundView 等属性访问子组件。更一般的,iOS APP 的视图一般都有这样的视图体系层次(View Hierarchy): UIWindow —> UIView —> UIImageView…,如下图所示:
view hierarchy
借鉴系统框架的思想,进行 iOS 视图层开发时,经常使用视图的组合,创建可复用的视图组件。组件内部使用多个基本系统视图以形成聚合体,来实现更复杂的功能,对外只暴露最上层组件的接口。

效果

  • 用户统一使用组合结构中的对象。
  • 更容易地添加新类型的组件,使设计更一般化。

装饰

定义

动态地给一个对象添加一些额外的职责,装饰模式比生成子类更为灵活。

类图

装饰
组件(Component)定义一个对象接口,具体组件(ConcreteComponent)定义了一个对象,可以给这个对象添加一些职责。装饰者(Decorator)维持指向组件对象的指针,并定义一个与组件接口一致的接口,具体装饰者(ConcreteDecorator)重载父类的接口并向内嵌的组件转发请求,并在转发请求前后执行一些附加动作。

实践

Objc 本身支持为类添加扩展(Category),而不会对原有类本身造成不好的影响。Category 中的方法成为了类的一部分,可以被其子类继承。这些额外的方法是动态添加的,不同于匿名扩展(Extension)在编译期绑定,所以要谨慎防止添加的方法和原有方法产生命名冲突,否则扩展中的方法存在于方法列表中更靠前的位置,在原有类查找方法列表时优先被发现,进而会覆盖掉原有方法的实现。在类拓展中运用方法调剂可以在不改变组件(Component)接口的前提下,实现动态添加职责的效果,如下代码所示:

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
+ (void)load {
SEL originalSelector = @selector(original_Method);
SEL swizzledSelector = @selector(swizzled_Method);
[self swizzleInstanceMethod:[MyObj class] originSelector:originalSelector otherSelector:swizzledSelector];
}

- (void)swizzled_Method {
// do stuff
[self original_Method];
// do stuff
}

+ (void)swizzleInstanceMethod:(Class)class originSelector:(SEL)originSelector otherSelector:(SEL)otherSelector {
Method otherMehtod = class_getInstanceMethod(class, otherSelector);
Method originMehtod = class_getInstanceMethod(class, originSelector);

BOOL didAddMethod = class_addMethod(class,
originSelector,
method_getImplementation(otherMehtod),

method_getTypeEncoding(otherMehtod));
if (didAddMethod) {
class_replaceMethod(class,
otherSelector,
method_getImplementation(originMehtod),

method_getTypeEncoding(originMehtod));
}
else {
// 交换2个方法的实现
method_exchangeImplementations(otherMehtod, originMehtod);
}
}

效果

  • 提供了更加灵活地向对象添加职责的方式。
  • 保持了组件(Component)类的简单性,避免了赋予组件类太多的功能。

外观

定义

为子系统的一组接口提供一个一致的界面,外观(Facade)模式定义了一个高层接口,这个接口使得这一子系统更容易使用。

类图

外观
外观(Facade)负责转发消息给子系统(Subsystem classes),子系统实现功能,处理外观(Facade)指派的任务。

实践

三方开源库 SDWebImage 用一句代码封装了下载图片的操作,如下:

1
2
[imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
placeholderImage:[UIImage imageNamed:@"placeholder.png"]];

这个对用户来说极为简单的 API 封装了其内部子系统的协同行为。在这次调用中,内部包括 UIImageView(WebCache) / UIView(WebCache) / SDWebImageManager / SDImageCache / SDWebImageDownloader 在内的类参与了整个下载过程,而这些类中复杂的操作都不需要客户去处理。其具体时序图如下图所示:
SDWebImage

效果

  • 为复杂的子系统提供了一个简单的接口,减少了客户要处理的对象数目。
  • 实现子系统与客户及其他子系统的松耦合,提高子系统独立性和可移植性。
  • 子系统之间仅通过外观模块通讯,简化它们的依赖关系。

享元

定义

运用共享技术有效地支持大量细粒度的对象。

类图

享元
享元(Flyweight)描述一个接口,通过这个接口享元可以接受并作用于外部状态。具体享元(ConcreteFlyweight)实现了享元接口,并为内部状态增加存储空间,具体享元必须是可共享的。非共享享元(UnsharedConcreteFlyweight)在对象结构层次中将具体享元作为子节点。享元工厂(FlyweightFactory)创建并管理享元,确保合理地共享享元。当用户请求享元时,享元工厂对象可以提供一个已存在实例或重新创建一个实例。

实践

iOS 开发中最常用到的表格控件 UITableView 实现了一套内存共享机制,即每个不同类型的表格单元 UITableViewCell 都维持一个特殊的标识符,作为一个享元加入到重用队列,当表格单元需要被创建时,先到队列中查找可共享重用的 UITableViewCell 享元,如果存在则出队取得这个 Cell 单元,并调用 UITableViewCell 本身的方法prepareForReuse做重用准备;如果不存在可用的Cell 单元,则再根据之前注册的类或 Xib 进行新对象的创建。UITableView 的dequeueReusableCellWithIdentifier:方法可方便地对享元进行创建和管理。

效果

  • 节省存储空间,实例总数减少,用计算时间换取对外部状态的存储。
  • 享元工厂帮助用户查找某个特定的享元对象,可引入某种形式的引用计数或垃圾回收机制。

代理

定义

为其他对象提供一种代理以控制对这个对象的访问。

类图

代理
代理(Proxy)保存一个引用使得代理可以访问实体(RealSubject),提供一个与实体接口相同的接口,这样代理(Proxy)可以用来替代实体,控制对实体的存取和创建。抽象体(Subject)定义实体(RealSubject)和代理(Proxy)的共用接口。

实践

NSProxy 类是平行于 NSObject 类的又一大基类,它遵从了<NSObject>协议,大体上可以当做 NSObject 类来使用。它能够转发消息到它代理的对象,并且可以改变自身或加载被代理的对象来响应消息。通常使用 NSProxy 类只需要实现两个方法:

- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel;

iOS 开发中用代理模式解决了重型资源的加载问题。使用重型资源的代理类替代真正的资源,并且在需要时负责实例化这个资源对象。

效果

  • 在访问对象时,引入了一定程度的间接性。
  • 实现了 copy-on-write 的优化方式。