MENU

Delegates vs Observers

作者:Ben Sandofsky,原文地址
翻译:BNCoding,如果翻译有误感谢指出。

一个结构良好的优秀app应用,必然包含一些功能职责定义良好并且协作高效的简单对象。当设计一个对象的时候,你一般都会仔细思考对象的属性和它的行为,但是设计对象之间的沟通协作的设计同样重要。

苹果的Cocoa框架只提供了有限的几种设计模式而且还没有文档指导我们该怎么更好的使用这些模式。现在我们来对比一下代理模式和观察者模式

当我不知道该使用那种模式的时候,我一般都会尝试使用代理模式。在一对一关系的对象之间这种模式是非常有用的。它的调试简单,并且与其它模式相比能够获得更多的编译时检查。

在大多数的单项通信中,通知是最理想的一对多关系。例如iOS键盘相关的情形。想象一下如果iOS使用应用程序delegate来通知键盘事件:

func application(application: UIApplication, willShowKeyboardAtFrame frame:CGRect) {  
    homeViewController.adjustKeyboardAtFrame(frame)      
    profileController.adjustKeyboardAtFrame(frame)
    messagesController.adjustKeyboardAtFrame(frame)
}  

当代码中的每一个控制器去处理其子控制器时,这就变成一个非常脆弱,容易出错的代码样板。如果你决定将它重构成一个易于维护的“全局调度”的对象时,你就会意识到你其实是在重新建造通知中心。

当滥用通知模式的时候,她就像是使用Cocoa框架程序员的goto语句。当一个通知触发了,一些问题可能在应用程序的任何地方出现,而且还很难预测这些问题出现的顺序。然后你就会开始一段令人发疯的调试旅程(画美不看)。

当对象间的通信变得复杂的时候协议往往适应性会更好。因为通知会使用很多不安全、无谓的运行时链接。

因此应该使用观察者模式来广播通知,使用代理来进行通信交流。

现在,把你的程序想象成现实生活中的商店。大多数的时候你需要的是一对一的交流,就像一个店员告诉顾客某件商品的位于什么地方。偶尔你才需要对充满顾客的店内广播通知,“本店将在半个小时内闭”。

当需要进行一对一交谈的时候,你是用了广播的方式,这就像是在玩telephone game,既不高效又可能导致信息丢失(注:作者的原意是,如果A想告诉D一件事,不必通过 电视中那种 A告诉B,B告诉C,C再告诉D游戏的方式,这样D可能得到的信息可能不完全正确而且效率也低)。如果有人准备用扩音器告诉你某个消息的时候,你快速喊停了对方,这样你也就不需要担心信息泄漏了。

示例

例如,我们要编写一个类似亚马逊购物一样的应用程序。这个应用程序肯定包含产品,而且需要展示产品。每一个产品都有图片、名称、价格。单个产品的实例可能会在多个地方展示,例如程序主页,你的购物车等等。

class Product {
    var photo:UIImage?
    private var photoURL:URL
    var price:Float
    var name:String
} 

我们决定对图片进行延迟加载(懒加载)。首先我们访问photo属性,这会返回nil并且触发下载动作。几分钟之后图片就下载好能够展示了。接下来我们将这部分操作进行功能划分,明确各部分的职责并且将各部分独立功能链接起来。

第一个问题:应该将网络操作部分的代码放在那里?我们可以把它放在我们的实体中;或者我们可以创建NetworkEntity,然后创建子类对象供app所有对象使用?

相比集成我们更喜欢组合(composition over inheritance)。创建一个其它对象来负责下载资源从而减轻Product对象的职责,这样对测试来说也更加容易。

当我们正在为app中几十个独立对象进行下载资源时,通过这个资源下载的单个示例以漏斗的形式处理这些请求时,操作将变得非常便利,而且我们还能控制请求的速度 或者直接取消请求。那么怎么将来两个部分联系起来呢?(点击:it isn’t a singleton)

我们将使用代理。当然,严格来说这里使用的是data source,但是它们的处理是一样的。

typealias ImageResponse = (UIImage?, NSError?) -> ()
    
protocol ProductDataSource:class {
    func product(product:Product, requestedPhotoWithURL url:NSURL, callback:ImageResponse)
}
    
class Product {
    weak var dataSource:ProductDataSource?
    var photo:UIImage?
    private var photoURL:URL
    var price:Float
    var name:String
}

注意我们并没有在delegate中任何地方说network。它可以使用Http或者从磁盘缓存中加载图片。

作为一个练习,如何像使用通知一样使用这个API?你可以围绕像PhotoCacheMissNotfication构建一些东西,但这么做一定是错误的。

通知会在整个应用中暴露状态。但是在某一个产品请求一个图片的时候是不是所有的对象都需要知道这个消息呢?这对于逻辑的蔓延来说很容易,并且将信息发布到将它看作API的环境中。这很容易就隐藏了一些会突然的副作用。

思考一个代表产品的tableview cell。上面有一个Add to Cart的按键,这个按键通知view controller将产品添加到购物车

class ProductCell:UITableViewCell {
    var addToCartButton:UIButton!
    var product:Product!
}  

如果使用target/action的话将会变得很困难,因为你需要知道到底是哪个产品会被添加。使用通知来链接视图与视图控制器的是一个反模式,就像在一个AddToCardNotificationuserInfo中包含产品信息。

如果在两个不同tab页有同一个view controller类的两个实例,那么会发生什么呢?一个tab可能是"卖的最好的"(Best Sellers),还有一个是"推荐给你的"(Recommended For You)"。两个view controoler都可以展现同一个产品,那么这个产品将会添加到购物车两次。

而且筛选过滤这些信息也变得更加的困难棘手。Swift中的值类型对象的参数在过滤的时候是无效的,而且如果使用条件判断语句的话又回让代码变的异常的冗长。因此这里应该使用代理而不是通知,我们可以定义一个这样操作,当tableview cell中的button点击时:

protocol ProductCellDelegate:class {
    func productCell(cell:ProductCell, didTapAddToCartForProduct product:Product)
}  

现在我们再来看看observers模式中的一个比较大的闪光点:那就是在像上面那种一个产品对应多个视图的这种一对多的关系中。当一个产品的图片顺利的更新完成后,它像展示页和购物车里面的产品同时更新。因此图片加载后,我们发出通知:

let ProductDidUpdateNotification:String 

如果我们想去减少那些每个视图都要跟踪的变化的话,我们可以通过userInfo的dictionary提供更新的上下文。

总结

在我的简单实例中,我使用了代理,但你也可以通过回调(callbacks)构建相似的关系。另外我使用了NSNotificationCenter而不是kvo,至于原因我会在以后进行进一步解释。