Plank简介:iOS的不可变模型生成

Rahul Malik | Pinterest技术主管,iOS核心体验

去年,我们的iOS团队全面改革了整个应用程序的体系结构。 这是一项巨大的努力,导致开发人员可以更快地迭代应用程序,更易于扩展,而全球Pinners的应用程序则快3倍。 我们的新系统严重依赖于并发。 UI渲染,图像下载,GIF解码和网络响应处理只是利用多个线程来提高性能的一些领域。 这意味着这些组件使用的对象必须是线程安全的,以避免错误和潜在的崩溃。 由于模型对象几乎遍历我们应用程序的所有组件,因此确保模型层可以安全地跨线程使用非常重要。

为了解决这个问题,我们转到了一个不变的模型层。 不可变对象与可变对象的不同之处在于,一旦创建它们就无法对其进行修改,这从本质上使它们成为线程安全的。 这使开发人员可以编写更易于推理的代码,因为一旦建立不变量就无法更改。 今天,我们是开放源代码的Plank,这是我们为实现此目的而创建的iOS不可变模型生成器。 Plank是用Swift编写的命令行工具,可生成不可变的Objective-C模型。 在本文中,我们将重点介绍一些主要功能以及其创建的动机。

动机

设计和维护模型层可能很繁琐且容易出错。 缺少简单的null检查或尝试序列化包含无法序列化的属性的对象可能会导致未定义的行为和崩溃。 手写模型还可能遭受其实现中的不一致问题,并且在序列化时可能导致不同的行为和策略。

以下是一些由于手写模型问题导致的常见错误和崩溃的示例。

通过Plank生成模型

让我们用这些字段创建一个表示Pin的模型。

定义架构

Plank将模式文件作为输入,因此我们需要创建一个。 这是Pin类型的架构。 您会注意到我们指定了模型的名称及其属性列表。 请注意,该链接指定了一个附加的format属性,该属性指示Plank使用更具体的类型,例如NSURLNSDate

产生模型

假设此架构另存为pin.json我们通过运行plank pin.json生成模型。 下面,我们将重点介绍Plank从您的模式生成的一些功能。

  $木板pin.json 
Plank创建的Pin类接口

您会注意到的第一件事是所有属性都是readonly 。 这使该类不可变,但是它并没有真正的用处,因为我们没有办法用任何值填充Pin的实例。 为了解决这个问题,我们需要一个抽象,它将采用一组值并产生一个不可变的对象。

变异和建造者

通过生成器类执行Plank生成的模型中的变异。 这是构建器模式的直接实现,Plank会为您生成它。 builder类是一个单独的类型,其中包含readwrite属性和一个将创建新对象的build方法。

JSON解析

现在,我们有了一个不可变的模型和一个构建器类来创建新实例。 但是,大多数应用程序不是静态的,并且依赖于从API返回的JSON数据。 这是我们的Pin模型的示例JSON响应。

为了正确处理此响应,我们不仅需要断言响应类型是正确的,而且还需要添加其他逻辑以将链接表示为NSURL的实例。 小心处理null值也很重要,以避免将属性设置为NSNull值或将null传递给需要nonnull参数的API。 这些错误可能导致无法预测的行为和崩溃。

Plank将创建一个名为initWithModelDictionary的初始化方法,该方法处理解析符合您的模式的NSDictionary对象。

序列化

如果要为应用程序建立脱机支持或在应用程序启动期间保留数据,则需要将模型存储到磁盘。 iOS上最常规的解决方法是在每个模型上实现NSSecureCoding 。 在Pinterest,我们使用PINCache作为冗余的直写模型缓存,该缓存还通过NSSecureCoding管理持久性。

Plank会为您生成NSSecureCoding实现。 由于所有本机类型均已可序列化,因此您可以免费获得此功能。

通过此实现,使用NSKeyedArchiver对象序列化

模型合并和部分对象实现

如果您的团队决定采用精美的新后端API,使消费者可以准确指定他们需要的字段(类似于Pinterest开发人员API或GraphQL),那么我们之前可能要求的Pin现在只能返回identifierlink或两者! Pinterest中的一个例子是,当您点击Pin时,我们会加载更多信息。

我们在这里需要考虑以下几点:

  • 如果您拥有图钉并收到包含更多信息的更新版本,您如何知道要更新的属性?
  • 如果属性为nil那么如何检测该属性是否已被设置而不是值为nil
  • 更新Pin之后,我们如何在整个应用程序中传播这些信息?

Plank通过保留在该实体的最新实例中设置的属性,使用常规的“最后作家获胜”方法来解决一致性问题。 我们将identifier值用作主键,该主键用于确定两个对象是否表示同一实体。 为了知道设置了哪些属性,我们在初始化或通过任何突变方法在模型内部内部跟踪此信息。

使用不可变模型,您必须考虑数据如何在应用程序中流动以及如何保持一致状态。 在每个模型类初始化程序的末尾,都会发布一个带有更新模型的通知。 这对于与数据一致性框架集成很有用。 借助此工具,我们可以跟踪模型何时更新,并且可以使用内部跟踪来合并新模型。

代数数据类型

随着您的应用程序变得越来越复杂,您的数据模型也会越来越复杂。 您可能会发现自己需要对一个属性进行建模,该属性可能是一组更通常称为代数数据类型(ADT)的类型中的一个特定变体。

在Pinterest上,我们将显示Pin的原因或归因,以告诉用户看到它们的原因。 这可能是因为它来自他们关注的另一位Pinner或董事会,或者可能是基于他们的兴趣的推荐。

让我们更新Pin模式,使其具有attribution属性,该attribution可以是UserBoardInterest 。 假设我们有分别在文件user.jsonboard.jsoninterest.json定义的架构。

用Plank表示代数数据类型

Plank会采用此attribution定义,并创建必要的样板代码来处理每种可能性。 它还通过生成代表您的ADT的新类来提供附加的类型安全性。

为“属性”属性隐式生成的ADT类

请注意,标头中声明了一个“匹配”函数。 这就是您提取ADT实例的真实基础值的方式。 这种方法保证了在编译时您已经明确处理了所有可能的情况,从而防止了错误并减少了使用运行时反射的需求,而这会影响性能。 以下示例显示了如何使用此匹配功能。

迅速

Plank是完全由Swift构建的,Swift是该任务的理想语言,因为它具有强大的类型安全性功能和优雅的语法。 我们利用递归枚举来定义架构的所有排列。 此外,尾随闭包和内联字符串插值被用于制作优雅的DSL以生成代码。

以下是我们如何利用Swift的语言功能创建DSL来表达生成的Objective-C切换语句的一种尝试:

这是使用我们刚刚创建的switch语句DSL的示例。 假设我们有变量dayOfWeek ,它是一个1到7的整数,分别代表星期一至星期日。 可以生成以下switch语句来说明dayOfWeek是否是周末。

建立强大的核心

Plank是用于构建和缩放不可变模型层的有价值的工具。 生成代码为我们节省了很多开发人员时间,并消除了常见错误的风险。 去年,它已在Pinterest上进行了严格的生产测试,我们很高兴与社区分享此技术。 如果您有改善Plank的建议,请随时在Github上提交问题或PR。 如果这些是令您兴奋的问题,我们正在招聘。

致谢:感谢所有我们的iOS开发人员在木板上使用和提供反馈,尤其是我的队友Wendy Lu,Brandon Kase,Levi McCallum,Bill Kunz,Jon Parise,Tim Johnsen,Connor Montgomery,Harry Shamansky和Garrett Moon对此提供了反馈职位,并请给Laurie Berger设计Plank徽标。