Communication-Patterns【译】

原文:Communication Patterns
Issue 7: Foundation · December 2013
By Florian Kugler

每个应用程序由包含多个或多个松散耦合的对象,这些对象常常需要相互通信才能完成应用的任务。在本文中,我们将介绍所有可用的选项,看看它们在苹果框架中如何使用的示例,最后总结何时使用哪种机制的的最佳实践建议。

虽然这个问题是关于Foundation框架,我们将超出Foundation框架中的部分通信机制–KVONotifications,还准备谈谈delegation, blocks, 还有 target-action

当然,在有些情况下,没有明确的答案说应该使用什么样的模式,而将选择归结为个人偏好问题,但也有很多情况(模式使用)是非常清晰明确的。

在本文中,我们经常使用“收件人”和“发件人”这两个术语,我们指的是在通信模式上下文中的意思,最好用几个例子来解释:表视图是发件人,而它的代理是收件人。一个核心数据管理对象上下文是它发布的通知的发件人,而不管它们是如何接收的。滑块是动作消息的发送者,实现这个动作的应答者是接收者。一个含有遵循KVO属性的对象,在变化的是发件人,而对应的观察者是收件人。明白窍门了吗?

模式

首先,我们将了解每个可用通信模式的特定特性。基于此,我们将在下一节中构建一个流程图,帮助您选择合适的工具。最后,我们将讨论苹果框架中的一些例子,以及他们决定在特定用例中使用特定模式的原因。

KVO

KVO是一种通知对象属性改变的机制。它是在Foundation框架上实现的,而且建立在Foundation框架之上的许多框架都依赖于它。如果需要阅读更多关于最佳实践的例子说明了如何使用KVO,请阅读丹尼尔的KVO和KVC文章

如果你只关心改变另一个对象的值,KVO是一种可行的通信模式。不过还有一些要求。首先,收件人(将接收变化消息的对象)——需要知道发件人(包含值变化的对象)。此外,收件人也需要知道发件人的寿命,因为它需要在发件人被释放者之前注销对其的观察。如果这些要求都满足,则这种通信可以是一对多,因为多个观察者可以注册来自己关心对象的更新。

如果你计划在Core Data对象上使用KVO,你需要知道,事情会有点不同。这与Core Datafault机制。一旦所观察对象变成了fault,它将在其属性上触发观察者,尽管它们的值没有改变。

通知

通知是一种很好的工具,可以在代码中相对无关的部分之间广播消息,即是是消息内容比较丰富的时候,而且您不必考虑还需要其他人参与。

通知可以发送任意消息,他们甚至可以通过UserInfo词典或子类NSNotification中包含一个payload(消息载体)。使通知具有唯一性的是发件人和收件人不必互相了解。它们可以用来在非常松散耦合的模块之间发送信息。因此,通信是单向的——您无法对通知作出回复。

代理

在苹果的框架中,代理是一种普遍的模式。它允许我们定制对象的行为,并对某些事件进行通知。对于代理模式,消息发件人需要知道收件人(代理),而不是反过来。耦合进一步松了,因为发件人只知道它的代理符合某个协议。

由于代理协议可以定义任意的方法,所以可以精确地将消息通信建模在您的需求里。您可以以方法参数的形式传递payload,代理甚至可以根据代理方法的返回值作出响应。代理是一种非常灵活和直接的通信模式,如果您只需要在两个特定对象之间进行通信,它们在应用程序体系结构中的位置上彼此相对接近。

但也有过度使用的授权模式的危险。如果两个对象紧密耦合在一起,而没有另一个对象,那么就不需要定义代理协议了。在这些情况下,对象可以知道对方的类型并直接进行通信。;两个新例子是uicollectionviewlayoutnsurlsessionconfiguration

Blocks

Block是相对最近才添加到Objective-C,最早在OSX 10.6iOS 4可用。Blocks通常可以作为之前使用代理模式实现的角色。然而,这两种模式都有一些优势和特别要求。

一个非常明确的标准:不要使用Block创建保留环。如果发件人需要保留这个Block,然而并不能保证对这个Block的引用将会置nil,那么每个从这个Blockself的引用,将成为一个潜在的保留环。

假设我们想实现一个表视图,但我们想用Block回调,而非它的代理方法来实现表视图的选择,比如这样:

1
2
3
self.myTableView.selectionHandler = ^void(NSIndexPath *selectedIndexPath) {
// handle selection ...
};

这里的问题是Self保留了表视图,而表视图必须保留Block,用以稍后使用它。表视图不能把这个引用置nil,因为它不能告诉它什么时候不再需要它了。如果不能保证这个保留环将被打破,那么将会一直保留发件人,那么在这里使用Block不是一个好的选择。

这并不会成为一个问题,NSOperation是一个很好的例子,因为它在某个时刻打破了保持环:

1
2
3
4
5
6
self.queue = [[NSOperationQueue alloc] init];
MyOperation *operation = [[MyOperation alloc] init];
operation.completionBlock = ^{
[self finishedOperation];
};
[self.queue addOperation:operation];

乍一看,这似乎是一个保留循环:Self保留队列,队列保留操作,操作保留完成块,completion block保留Self。但是,将操作添加到队列将导致在某个时间点上执行该操作,然后将它从队列中删除。(如果它不被执行,我们就有一个更大的问题。)一旦队列删除操作,保留循环就被破坏了。

另一个例子:假设我们实现视频编码器类,在我们称之为一个encodewithcompletionhandler方法中。为让这不出问题,我们必须保证编码器对象在某一刻对于这个block的引用置空。在内部,这应该是这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@interface Encoder ()
@property (nonatomic, copy) void (^completionHandler)();
@end

@implementation Encoder

- (void)encodeWithCompletionHandler:(void (^)())handler
{
self.completionHandler = handler;
// do the asynchronous processing...
}

// This one will be called once the job is done
- (void)finishedEncoding
{
self.completionHandler();
self.completionHandler = nil; // <- Don't forget this!
}

@end

一旦我们的工作完成了,会调用completion block,然后将其置空。

如果我们所调用的消息必须返回该方法调用的一次性响应,那么Block是非常合适的,因为这样我们可以打破潜在的保留循环。此外,如果有助于可读性,使处理消息与消息调用的代码写在一起,就不去使用Block。沿着这些线路,很常用的Block的情况下是(方法的)completion handlers,错误处理等等。

Target-Action

Target-Action用于响应用户界面事件发送消息的典型模式。两UIControliOSNSControl/NSCell 在Mac上都支持这种模式。Target-Action建立了消息在发件人和收件人之间的松散耦合关系。该消息的收件人不知道发件人,甚至不需要知道将会接受什么消息。如果Targetnil空的,Action将顺着响应链往上,直到找到响应它的对象。在iOS上,每个控件甚至可以与多个TargetAction对相关。

基于target-action的通信有一个限制,发送的消息不能携带任何自定义的payloads。在Mac上的操作方法总是发件者作为第一个参数接收。在iOS上,可以将发件者和触发动作的事件作为一种参数来接收。但除此之外,还没有办法让一个控件用action将消息发送给其他对象。

做出正确的抉择

根据上面所描述的不同模式的特点,我们构建了一个流程图,帮助您在某种情况下对使用哪种模式,做出良好的决策。作为提醒:这个图表不一定是最终的答案;可能还有其他同样有效的选择。但在大多数情况下,它应该指导你为这个场景选择合适的模式。

Decision flow chart for communication patterns in Cocoa

本图值得进一步解释一些其他的细节:

(图中)其中的一个方块表示:发件人是支持KVO。这并不只是意味着当问题中的值发生变化时,发件人将发送KVO通知,而且观察者要知道发件人的生命周期。如果发件人存放的属性是weak,它可以在任意时间置空(nil),而观察者会发生内存泄漏。

在底排另一方块表示,消息是直接响应方法调用的。这意味着,方法调用的接受者需要回应该方法的调用者,作为对这个方法调用的一个直接响应。这也意味着,当这个方法调用时,代码在同一个地方处理此消息是有意义的。

最后,在右下角有一个决策问题:发件人可以保证对Block的引用将会置空(nil)吗?这个联系到了上面的基于block的API和潜在的保留环的讨论。如果发件人无法保证这些block所持有的引用将在某个时刻置空(nil),那么你将会遇到保留环的麻烦。

框架示例

在本节中,我们将从苹果的框架中看一些例子,看看前面所说的决策流程是否有效,以及为什么苹果选择这些模式。

KVO

NSOperationQueue使用KVO来观察它的操作状态(是否完成、是否在执行、是否取消)。当这个状态变化时,这个队列的得到一个KVO的通知,为什么操作队列对此用KVO呢?

收件人的消息(操作队列)清楚地知道发件人(该操作)和通过保留来控制其生命周期。此外,这种情况下,只需要一个单向的通信机制。说到如果操作队列中的操作值的变化只感兴趣,这个答案是不很清晰。但我们至少可以说,有什么要传递的(比如状态改变)可以封装成值的改变。由于状态属性已超出操作队列及时了解操作的状态的需求,在这种情况下使用KVO是一个合乎逻辑的方案。

Decision flow chart for communication patterns in Cocoa

KVO是不是唯一有效的选择。我们还可以设想,操作队列成为操作的代理,然后操作将调用如operationDidFinishoperationDidBeginExecuting的方法,将它状态信号的变化通知到队列。虽然这将不那么方便,因为操作除了调用这些方法之外还要保持其状态属性的更新。此外,队列必须跟踪所有操作的状态,因为它不能再请求它们了。

通知

Core Data使用notifications来通知对象内容的变化事件(NSManagedObjectContextObjectsDidChangeNotification)。

通知的变化是由托管对象的内容发送的,因此我们不能假定消息的收件人一定知道发件人。由于消息的起源显然不是UI事件,所以可能有多个收件人对它感兴趣,而它所需要的只是单向通信通道,这种场景中notifications是唯一可行的选择。

Decision flow chart for communication patterns in Cocoa

代理

表视图的代理完成很多功能,从管理附属视图到编辑和跟踪屏幕上的单元格。在这个例子中,我们将看看TableView:didselectrowatindexpath:方法。为什么这个要作为代理方法来调用?为什么不适用target-action模式呢?

正如我们在上面的流程图中所概述的那样,target-action只有在不需要传输任何自定义有效载荷时才有效。在选择的情况下,collection视图告诉我们点击一个cell时不仅选择了一个cell,而且还通过传递索引路径选择了哪个cell。如果我们保持这个要求发送索引路径,我们的流程图指导我们直接进入代理模式。

Decision flow chart for communication patterns in Cocoa

在选择cell的消息中,假如不发送索引路径,而是一旦我们收到的消息,通过询问表视图找回选定的cell呢?这将是非常不方便的,因为我们将不得不做记录目前选择的cell,在多个选择中以确定哪个cell是新选择。

类似地,我们可以通过观察选定的索引路径属性的更改,当表视图中点击的改变时, 得到相关的通知。然而,我们也遇到了同样的问题,正如上面提到的,如果我们自己的不做记录,将无法区分哪些cell是最近选择/取消选择的。

Blocks

一个基于block的API,比如以- [ NSURLSession dataTaskWithURL:completionHandler:]为例。从调用者到URL加载系统间的通信是什么样的?首先,作为这个API调用者,我们熟悉消息的发送者,但我们不保留它。此外,它是一个单向的通信,直接连接到dataTaskWithURL:方法的调用。如果我们将这些因素纳入流程图中,那么将直接结束这个基于block的通信模式。

Decision flow chart for communication patterns in Cocoa

还有其他选择吗?当然,苹果自己的NSURLConnection就是最好的例子。NSURLConnectionObjective-Cblock之前就创建了,所以他们时需要采取不同的路线,并且使用代理模式实施通信。一旦block是可用的,在OS X 10.7iOS 5中,苹果将方法sendAsynchronousRequest:queue:completionHandler:NSURLConnection方法中,所以你不需要再为简单的任务设置代理了。

因为NSURLSession是刚刚在OS X 10.9iOS 7上添加的一个非常新的API,而block现在是作为这种通信模式的选择(NSURLSession也有一个代理,但是是作为其他用途的)。

Target-Action

对于target-action模式的一个明显的用例就是按钮。按钮不需要发送任何信息,除非他们已经点击(或轻拍)。从这个意义来说,target-action是UI事件通知App中非常灵活。

Decision flow chart for communication patterns in Cocoa

如果目标是指定的,行动的消息将被直接发送到该对象。然而,如果目标是nil,行动消息会顺着事件链往上中寻找可以处理它的对象。在这种情况下,我们有一个完全解耦的通信机制,发件人不必知道收件人,而不是反过来。

target-action模式对UI事件来说是完美的。没有其他的通信方式可以提供这样的功能。Notifications是对发件人和收件人解耦方面最接近的,但使target-action特别的是对响应者链的使用。只有一个对象对得到的action作出反应,而且action顺着响应链通过定义好的路径,直到它被某个对象获取到。

总结

一开始,对象之间的通信模式看起来似乎很多,在选择哪种模式时经常感到模棱两可。但是一旦我们对每种模式进一步了解,它们都有非常独特的要求和功能。

决策流程图是一个很好的开始,你可以在选择特定模式时非常清晰,但当然不是所有问题的结束。如果它符合你使用这些模式的方式,或者你认为有什么遗漏或误导的话,我们将很高兴收到你的来信。

文章作者: MichaelMao
文章链接: http://frizzlefur.com/2017/08/22/Communication-Patterns【译】/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 MMao
我要吐槽下