翻译:https://engineering.pinterest.com/blog/immutable-models-and-data-consistency-our-ios-app
今年早些时候,为了让iOS客户端反应更快,体验更简明,特别是针对非美国用户,我们对iOS客户端进行了重构。一个重构的目标就是,将客户端的模型层变为完全不可变。这篇文章中,我将讨论这种做法背后的动机,探讨我们的新系统在更新模型,加载网络接口数据和保持数据一致性方面的做法。
不可变模型?
“不可变模型”是近期经常听到讨论的术语,而且很多客户端已经转为不可变模型。不可变意味着模型一旦初始化完成之后就不能再修改。为什么要使用呢,可变模型的主要问题是数据处于共享状态。
考虑下面这种情况,在一个可变模型系统中,A和B都引用了C。
如果A修改了C,A和B都将得到变化后的值。这很好,但是如果B不期望如此,就会发生问题。
举个例子,在消息界面中,有两个其他的用户。每个消息对象都有一个“用户”属性。
当我停留在这个页面上时,客户端中的其它部分把Devin从对话中移除(可能是接收了服务器的回应,更改了数据模型)。这时我点击了第二行想屏蔽Devin,会关联用户列表中的第二个对象。将要返回的是Stephanie而不是Devin,最终我把错误的人加入了黑名单。
不可变对象是线程安全的。以前,我们得担心一个线程写数据的时候,另一个线程正在读数据。在新系统中,对象自从创建后就不可以更改,所以我们可以安全的并发的使用多线程读取数据,而不用担心数据错乱。这让开发变得更轻松,因为客户端可以越来越支持并发和多线程。
更新数据模型
自从数据模型创建后就变得完全不可变,唯一更新或者说改变的方法是构造一个新的模型。有两种方式来做这件事:
-
使用字典来初始化模型(通常来自json报文)
User *user = [[User alloc] initWithDictionary:dictionary];
-
使用构造器对象,通常是拥有数据模型所有属性的可变描述。可以基于存在的数据模型中创建一个构造器,修改你想修改的属性,然后调用initWithBuilder方法来返回一个全新模型。(关于这一点后续文章会有更多介绍)
// Change the current user's username to “taylorswift” UserBuilder *userBuilder = [[UserBuilder alloc] initWithModel:self.currentUser]; userBuilder.username = @"taylorswift"; self.currentUser = [[User alloc] initWithBuilder:userBuilder];
加载和缓存接口数据
我们的接口允许从服务器拉取部分模型数据,模型属性的部分子集。比如在图钉列表页,只需要图片链接和描述即可,并不需要全部数据,直到用户进入图钉聚合页才需要食谱原料(recipe ingredients)属性。
客户端中有一个PINCache数据缓存中心,PINCache是一个数据模型缓存库,已经开源。缓存的key是服务器为每个模型提供的唯一的ID。每当我们从服务器得到数据,先从缓存中心里检查模型是否存在。如果存在,将服务器的新的数据和模型存在的属性进行融合,生成新的模型。新的模型将替代缓存中旧的模型。这样,缓存的模型总是拥有我们接收到的所有最新属性。
数据一致性
模型改变(比如新建一个模型),应该通知到展示模型的视图。我们之前使用KVO来完成,但是KVO对不可变不起作用,他只观察一个实例的改变。现在我们使用基于NSNotificationCenter的通知系统来广播数据模型的更改。
观察变化
视图或者视图控制器可以注册成为模型更新的观察者。这个例子中,消息页面控制器监听了他的消息对象的更新通知。控制器需要知道模型变化,因为新的模型可能会更新属性。
下面的代码,创建了一个观察者用来监听模型的变化,观察的名字是模型的唯一标识符。在这个方法下,使用基于block的NSNotificationCenter接口,这样可以比较方便的管理观察着的生命周期。
[self.notificationManager addObserverForUpdatedModel:self.message block:^(NSNotification *notification) {
// Update message view here!
}];
notificationManager只是一个强引用观察者的NSObject,由于它是视图控制器的属性,得确保控制器销毁的时候,它也会销毁,并且能够解除对所有观察者的引用关系。
广播变化
当消息对象更新,会广播一个更新通知
Message *newMessage = [[Message alloc] initWithBuilder:newBuilder];
[NotificationManager postModelUpdatedNotificationWithObject:newMessage];
postModelUpdatedNotificationWithObject方法会查找缓存中心中拥有相同标识符的对象,并将这个对象广播出去。
更新界面
收到通知后,新的对象可以通过NSNotification的object属性获得。视图控制器可以用新的模型对象做任何事情。
__weak __typeof__(self) weakSelf = self;
[self.notificationManager addObserverForUpdatedModel:self.message block:^(NSNotification *notification) {
__typeof__(self) strongSelf = weakSelf;
Message *newMessage = (Message *)notification.object;
strongSelf.usersInMessageThread = newMessage.users;
[strongSelf.tableView reloadData];
}];
待更新
对于我们这种体量的客户端来说,完全替换为不可变模型不是件易事,在这个过程中,我们也创造了一些很棒的工具来辅助开发。下一片文章,将介绍我们是如何进行自动合成模型类以及其它知识。