Swift 4.2可降解:异构集合📚

继去年发布Codable协议之后,许多iOS开发人员一直在忙于删除沉重的自定义JSON解析器,并在模型层中将它们替换为对Decodable的流畅且轻量级的构造……我也不例外:在最近的项目中作为一个客户,我很高兴增强大型应用程序的模型层,其目标之一就是完全符合Codable。

挑战1:嵌套的异构集合

最初,过渡到Decodable的过程很顺利(“祝您删除代码愉快!” )。 但是,自定义JSON解析器的删除导致重要的类类型映射的删除-原本功能强大的Codable协议未直接支持Codable
为了举例说明我在说什么,请考虑以下几点:

在这种非常简单的情况下,我们有一个超类Pet,它被两个类CatDog继承,从而允许一个人拥有一个超类Pet的单个集合,而集合中的实际对象是Pet, Cat or Dog类型。 从JSON解码Person对象时会发生问题,因为“宠物”列表中的对象不是同一类型:

宠物列表将在Person初始化程序中像这样解码: container.decode([Pet].self, forKey: .pets) ,但这样做将检索超类Pet对象的列表,从而失去所有子类属性。 用CatDog代替Pet也是不够的,因为我们对每种不同的类型(包括它们各自的属性)都感兴趣。
同时使用JSONSerialization反序列化JSON以使用类型区分JSONSerialization进行映射,似乎完全消除了使用Codable的好处,因为这是使用旧JSON解析器完成的方式。

解决方案:集中类映射

幸运的是,我并不孤单地遇到这个问题:Tom Stoffer写了一篇很棒的文章,介绍如何处理嵌套在Decodable对象中的异构列表。 尽管此解决方案在某种情况下是好的,但对于大型项目而言,这并不是一个非常干净的解决方案,因为我们可能会重复类型映射代码,因此我们可能会遇到多个对象相同的异构列表。
为了解决这个问题,我们可以将类型信息(映射)提取到类家族的集中位置。 这可以通过利用快速枚举上的函数来完成。 目标是通过公开用于检索正确映射的类型的函数以及JSON有效内容中类型鉴别符的键,来创建一个表示相关类家族的枚举。 因为我们希望解决方案尽可能通用,所以我们可以编写一个协议来定义所需的公开信息:

注意:仅在您为鉴别器使用不同键的情况下,才真正需要鉴别器类型。 (在我正在从事的项目中,我有两个不同的键)。 此外,鉴别变量是静态的-稍后会详细介绍。

ClassFamily协议允许我们创建一个描述任何对象族的枚举。 让我们看一个例子,说明我们的Pet案例的枚举是什么样的:

太好了,这个枚举现在描述了我们的宠物对象家族!

然后,我们可以直接在Person类的可解码的初始化程序中使用它,如下所示:

但老实说,此解决方案仅提取了映射,并没有真正使初始化程序更简洁。
因此,为了解决这个问题,我们将在KeyedDecodingContainer:的扩展中创建解码函数的泛型重载KeyedDecodingContainer:

这样做可以使我们极大地清理Person的初始化程序:

我们已经完成了可解码对象的嵌套异类列表的解决方案-干净整洁,不是吗?

但这不是全部…

挑战2.异构集合作为返回类型

…如果异构集合未嵌套在Decodable对象中怎么办? 例如,考虑以下情况: PersonPets集合不是属性,而是在运行时通过API调用从服务器获取的:

在这种情况下,问题仍然存在: [Pet]类型的解码将导致带有名称的未知宠物的列表。 使用CatDog也是不够的,并且为了像Tom Stoffer的方法一样,我们不像在Person类的初始化程序中那样具有带键控容器的Decoder对象。
令我惊讶的是,我在Stackoverflow或其他地方找不到很多类似情况的信息,然后返回反序列化JSON以读取鉴别符类型,然后进行解码,这对我来说是不可接受的。

解决方案:包装器类!

因此,经过数小时的类型推断问题和编译错误,我设法使用包装器类和一些枚举提出了一个通用解决方案。

这个想法是能够通过利用相同的类家族枚举来使用与嵌套集合相同的方法。 为此,我们必须围绕可解码类(及其子类)创建一个包装器类,然后可以将其映射到正确的类型。
让我们看一下如何实现它。 包装器必须是可解码的,以便我们使用JSONDecoder直接对其进行解码。 此外,它需要保留对我们希望创建的对象的引用。 当我们使用泛型时,对象类型自然也应该是泛型的。
为了在解码时处理映射,我们将实现init(from decoder: Decoder)在该过程中,我们将首先使用DiscriminatorClassFamily枚举进行解码,以便将其余数据映射到正确的类型(就像以前一样)。
该实现可以在下面看到:

请注意包装器如何采用两种通用类型: TClassFamily枚举,而U是对象族的可解码超类。 为了解释初始化程序中发生的事情,我们首先像往常一样获得了解码器的容器。 然后,我们尝试对族进行解码(请记住枚举对应于鉴别符字段“ type ”的值)。 在这里,我们还注意到了为什么有必要在ClassFamily协议中将discriminator属性ClassFamily static (此时,我们只是在内存中没有枚举)。
在内存中有类家族枚举时,我们调用getType()并将值getType()转换为超类U的类型,以便能够对其调用.init(from: decoder)AnyObject尤其重要,因为该函数返回无法解码的AnyObject类型。
结果是一个ClassWrapper对象,该对象现在包含正确映射的对象。 现在可以在Person类的getPets()函数中使用它:

太好了,它有效! 但是,每当我们要解码一个异构列表时,就必须显式地编写通用的ClassWrapper对象和ClassWrapper对象-并不是很方便。 因此,我们通过向JSONDecoder类添加扩展来使JSONDecoder

这种JSONDecoder扩展的好处是,所有其他类都不再需要了解ClassWrapper对象,因此,现在可以将该类设为扩展内的private类。
有了锦上添花,我们就可以像这样解码Pet对象的异构列表:

这“包裹”了解决方案。 为了支持其他对象系列,我们只需要实现一个与新上下文相对应的新ClassFamily枚举即可。 使用这种方法的一些优点是,它提供了更简洁的初始化方法和一个集中的映射位置。 如果几个类具有相同家族的异构列表,这对于防止代码重复特别有用。

结论

在本文中,我们了解了使用Swift 4.0 Decodable协议时如何证明异构集合是一个真正的挑战。 但是,我们还看到了初始化程序中的一些调整如何通过使用Tom Stoffer的方法来解决嵌套集合的挑战。 在他的方法的基础上,我们看到了关于如何在枚举中集中对象映射的解决方案。 此外,我提出了针对直接返回异构集合的挑战的解决方案。 当集合未嵌套在另一个可解码对象中时,通用包装器类很有用。
该解决方案的优点包括更干净的初始化程序,更少的代码重复和适当的代码分离。 实施过程肯定改善了我客户的项目,希望它也能给您带来启发。

资料下载

如果您有兴趣,可以单击此处下载本文的全文。