转载

现代化的 Core Data

让我们使用 Swift 来给那些老旧的 Objective-C API 注入新的活力吧!在 try! Swift 的本次讲演当中,Daniel Eggert 给出了一个使用 Core Data 的示例,展示了如何通过使用协议和协议扩展,让代码更具有可读性、更少出错。

今天我要谈论的是 现代化的 Core Data 。不过,本次讲演的核心并不是 Core Data;这次讲演是关于如何在 现代化的 Swift 代码 当中使用那些老旧的 API 的( Swift 用起来非常有趣,我们同样也希望让老旧的 API 也同样有趣! )。

本次讲演主要有两个目标:1) 让代码更易读,2) 让代码更少出错。为此,我们将使用两个工具:协议和协议扩展来帮助我们实现目的。

我们将使用 Core Data 来作为示例;这个是个绝佳的例子,因为 Core Data 已经有 12 年的历史了,专门为 Objective-C 而生,专门为 Objective-C 而写,并且它是动态的,而且还不是类型安全的。在 Swift 当中,我们并不希望出现太多的动态内容,并且我们也希望使用类型安全的内容。让我们看一看如何将这两个世界桥接在一起。去年我同 Florian 一起写了一本书;我将为大家展示书中的几个例子,因为它是百分百用 Swift 写的,但是写的内容全是关于 Core Data 的。

保留既有 API 的设计理念,但是让其更易于使用

我们希望保持既有 API 的设计理念( 我们同样还希望我们的代码既优秀又易读 ),但是代码又不脱离 Core Data 的设计,还要求易于使用。

在 Core Data 当中,实体 (Entity) 和类 (Class) 之间拥有非常强的动态耦合 (dynamic coupling) 关系。实体是您所定义的数据模块所在的地方;类( 您的 Swift 类 )是您自定义逻辑的地方。通常情况下,实体和类之间是一对一映射的:一个实体直接映射到一个类上。因为 Core Data 和 Objective-C 的历史遗留问题,这段代码看起来会非常奇怪,特别是您想要在 Core Data 中插入一个新对象的时候( 如下所示 )。

let city = NSEntityDescription   .insertNewObjectForEntityForName("City",     inManagedObjectContext: moc) as! City

这里有三个东西是我很不喜欢的:1) 这段代码真的长,2) 您需要使用 “city” 字符串,这导致编译器不能在我输入错误的时候帮助我指出这个错误,以及 3) 最后我们要执行类型强制转换( 这看起来非常丑,我们不喜欢在 Swift 代码当中出现这种东西! )。

let city: City = moc.insertObject()

如果我们要插入一个 City 对象,我们只需要调用 insertObject() 即可。这样做的话,Swift 编译器就可以帮助我们做很多繁琐的工作:

1:创建一个协议

首先,我们需要创建一个协议 ManagedObjectType ;该协议定义了 entityName也就是之前的那坨字符串 )。

protocol ManagedObjectType {   static var entityName: String { get } }

2:让我们的类实现这个协议

我们回到我们的 City 类当中来( ManagedObject 类),这时候我们希望让 City 类能够实现这个协议。我们对 City 类进行了扩展,然后实现了这个协议,也就是说, entityName 为 “City”。

final class City: ManagedObject {   @NSManaged public var name: String   @NSManaged public var mayor: Person   @NSManaged public var population: Int32 }  extension City: ManagedObjectType {   static let entityName = "City" }

3:向上下文中添加扩展

我们已经将 City 实体与 City 类建立了关联( 如上所示 );现在我们就可以对上下文 (context) 进行扩展,然后添加我们之前所使用到的方法:

extension NSManagedObjectContext {   func insertObject<A: ManagedObject where A: ManagedObjectType>() -> A {     guard let obj = NSEntityDescription       .insertNewObjectForEntityForName(A.entityName,         inManagedObjectContext: self) as? A else {           fatalError("Entity /(A.entityName) does not correspond to /(A.self)")     }     return obj   }    ... }

我不会详细介绍所有的细节 )这本来是我们之前所撰写的旧有代码,但是现在我们将其很好地封装了起来。我们可以在这里提取 City 字符串( 我们此前就获取得了 ),同样还可以执行相应的类型转换。这些东西都可以很好地藏在一个地方。

4:最终结果

一旦我们使用了这类既优秀又易读的代码的话,那么几乎是不可能会犯错的。

let city: City = moc.insertObject()

键值编码 (Key Value Coding) 是一个 很有历史的玩意儿 了,尤其是在 Swift 代码当中,它显得与其格格不入。键值编码是非常动态化的,在十二年前它就是所谓的热门。它被 Core Data 用于键值观察 (Key value observing),但是它很容易出现错别字和错误,并且它并不是类型安全的( 我们不喜欢这种情形! )。让我们看一看在 Core Data 中它是什么样的:

final class City: ManagedObject {   @NSManaged public var name: String   @NSManaged public var mayor: Person }  func hasFaultForRelationshipNamed(name: String)

如果我们回顾一下我们的 City 类,我们可能会想要使用这个 Core Data 方法,即 hasFaultForRelationshipNamed ,这意味着我们必须要传递某个参数进去,而这个参数是一个字符串。我们可能会这样用:

final class City: ManagedObject {   @NSManaged public var name: String   @NSManaged public var mayor: Person }  func doSomething(city: City) {   if city.hasFaultForRelationshipNamed("mayor") {     // Do something...   } }

我们执行了一些“操作”,我们必须要将匹配我们属性的字符串 “mayor” (对应 mayor 属性)传递到这个方法当中。再次强调,编译器并不能帮助我们识别这里出现的错误;如果这里发生了错误,那么在运行时它就会发生崩溃。

为了向您展示这种用法为何非常糟糕,Core Data 还包含了这些全都在使用键值编码的方法( 具体请查看视频 )……我们想要对其进行改进( 具体请查看下方内容 )。

if city.hasFaultForRelationshipNamed(.mayor) {   // Do something... }

这种写法非常易读( 与之前的例子类似 ),但是我们还是希望让编译器能够检查 (.mayor) 是合法的键值,并且 Xcode 还能够自动补全。 我们该如何办到这一点呢?

1:创建一个协议

我们以协议开始,即 KeyCodeable ;它只有一个别名 Key

protocol KeyCodable {   typealias Key: RawRepresentable }

2:添加键值枚举

我们回到 City 类当中,我们对其进行扩展,让其实现该协议。

final class City: ManagedObject {   @NSManaged public var name: String   @NSManaged public var mayor: Person }

现在我们在 City 当中拥有了一个嵌套的类型,也就是说 Key 是属于 City 的,我们在其中包含了它的两个属性( namemayor ),将其定义为枚举值。

final class City: ManagedObject {   @NSManaged public var name: String   @NSManaged public var mayor: Person } extension City: KeyCodable {   public enum Key: String {     case name     case mayor   } }

3:使用协议扩展

接下来我们使用协议扩展( 如同我们之前做的那样 )来构建一个简单的版本。 hasFaultForRelationshipNamed 方法现在接收的是 Key 类型而不是字符串类型。

extension KeyCodable     where Self: ManagedObject, Key.RawValue == String {    func hasFaultForRelationshipNamed(key: Key) -> Bool {     hasFaultForRelationshipNamed(key.rawValue)   }    [...] }

接下来我们就可以在下面实现这个方法了,它接受 Key 类型为参数,编译器会告诉我们输入是否有效,并且 Xcode 还会为我们提供自动补全功能。现在我们已经实现了类型安全的键值编码。

if city.hasFaultForRelationshipNamed(.mayor) {   // Do something... }

通过添加默认方法让检索请求变得更完美

对于 City 类来说,如果我们想要检索所有城市的话,我们通常会这么做:

final class City: ManagedObject {   @NSManaged public var name: String   @NSManaged public var mayor: Person   @NSManaged public var population: Int32 }  let request = NSFetchRequest(entityName: "City") let sd = NSSortDescriptor(key: "population", ascending: true) request.sortDescriptors = [sd]

这看起来并不是很糟糕(只有三行代码)……但是仍然还是有些不那么如意。我们可以对其进行优化!:muscle:

我们想要的是什么样子的

final class City: ManagedObject {   @NSManaged public var name: String   @NSManaged public var mayor: Person   @NSManaged public var population: Int32 }  let request = City.sortedFetchRequest

我们只需简单地请求 City 类,直接给我们 sortedFetchRequest 就可以了。所有的逻辑都已经完全封装到里面了;再强调一遍,这种做法更容易阅读,并且更难以犯错误。 我们该如何实现这一点呢?

1: ManagedObjectType 协议

protocol ManagedObjectType {   static var defaultSortDescriptors: [NSSortDescriptor] { get } }

我们使用相同的协议,向里面添加 defaultSortDescriptors 属性,它负责将类与类之间的逻辑顺序关联起来。

2:实现协议

对于 City 类来说我们只需要这样做:让 City 类实现我们的这个协议,这样它的默认排序将会按照人口进行排序。

extension City: ManagedObjectType {   static var defaultSortDescriptors: [NSSortDescriptor] {     return [NSSortDescriptor(key: City.Key.population.rawValue, ascending: true)]   } }

3:协议扩展

接下来我们就可以实现这个很优雅的 sortedFetchRequest 方法了,只要使用协议扩展就行(用另一个扩展),我们可以将 entityName 提取出来( 因为我们在之前已经完成了这个操作 ),我们获取这个 defaultSortDescriptors ,接着返回检索请求即可。

extension ManagedObjectType {   static var sortedFetchRequest: NSFetchRequest {     let request = NSFetchRequest(entityName: entityName)     request.sortDescriptors = defaultSortDescriptors     return request   } }

4:优点:moneybag::moneybag::moneybag:

使用短短一行代码: let request = City.sortedFetchRequest ,我们就完成了对 City 创建检索请求的操作。如果我们有一个 Person 类的话: let request = Person.sortedFetchRequest 同样也是有效的,并且它看起来很优雅、很简洁、通俗易懂。我们一直尽力在让代码更加易懂。

额外的一点

在 Core Data 中,我们通常在检索的时候使用谓词 (Predicates);我们可以将两者合二为一,然后创建一个新的方法: sortedFetchRequestWithPredicateFormat

let request = City.sortedFetchRequestWithPredicateFormat("%K >= %ld",   City.Key.population.rawValue, 1_000_000)    extension ManagedObjectType {   public static func sortedFetchRequestWithPredicateFormat(     format: String, args: CVarArgType...) -> NSFetchRequest {       request = sortedFetchRequest()       let predicate = withVaList(args) { NSPredicate(format: format, arguments: $0) }       request.predicate = defaultPredicate       return request     } }

我们使用既有的 sortedFetchRequest 方法(我们之前就创建的),然后创建一个谓词,然后将其返回即可。我们可以使用这种方式让代码更加易读。

类型转换 - NSValueTransformer

NSValueTransformerFoundation API 的一部分,它同样也可以用在 Core Data 当中,并且还可以用于进行绑定操作(AppKits)。当然,在 Swift 世界当中这玩意儿也是非常诡异的:您需要使用继承才能够使用它,并且它还不是类型安全的。

让我们来看一个例子:我们需要使用一个 UUID,并且是使用字符串来进行表示的,当然也有可能是从服务器获得的一串字符串,然后我们希望将其转变为 UUID,也就是将字符串与原字节之间相互转换。如果用传统方法的话,您可能会这么做:

final class UUIDValueTransformer: NSValueTransformer {   override static func transformedValueClass() -> AnyClass {     return NSUUID.self   }   override class func allowsReverseTransformation() -> Bool {     return true   }   override func transformedValue(value: AnyObject?) -> AnyObject? {     return (value as? String).flatMap { NSUUID(UUIDString: $0) }   }   override func reverseTransformedValue(value: AnyObject?) -> AnyObject? {     return (value as? NSUUID).flatMap { $0.UUIDString }   } }  let transformer = UUIDValueTransformer()

您需要使用继承,然后实现这四个方法,然后将其实例化。这看起来并不是很糟糕,但是我们可以做得更好:

我们希望有这样一个 ValueTransformer ,其中的闭包可以将字符串转换为 NSUUID ,另一个闭包可以将 NSUUID 转回字符串:

let transformer = ValueTransformer(transform: {     return NSUUID(UUIDString: $0)   }, reverseTransform: {     return $0.UUIDString })

需要注意的是:我们并不需要知道正在转换的类型是什么;相反,Swift 编译器可以帮助我们很好地做到这一点。首先,第一部分是这样:

class ValueTransformer<A: AnyObject, B: AnyObject>: NSValueTransformer {   typealias Transform = A? -> B?   typealias ReverseTransform = B? -> A?   private let transform: Transform   private let reverseTransform: ReverseTransform   init(transform: Transform, reverseTransform: ReverseTransform) {     self.transform = transform     self.reverseTransform = reverseTransform     super.init()   } }

这是一个包含两个类型 A 和 B 的泛型类,也就是我们需要相互转换的两个类型。然后我们有两个闭包,从 A 转换为 B,以及从 B 转换为 A。我们同样还实现了这四个方法。

然而,我们可以以这种非常优雅的方式来实现和实例化 NSValueTransformer非常的现代化,也非常的 Swift 化 )。

let transformer = ValueTransformer(transform: {     return NSUUID(UUIDString: $0)   }, reverseTransform: {     return $0.UUIDString })

封装 Block API - 存储

闭包 (Blocks) 与我们所使用的某些 API 相比,是一个非常新颖的东西。Core Data 已经有十二年的历史了,而闭包可能只有 5 到 6 年的历史;一旦我们换上 Swift 的话,使用闭包将变为一件很有意思的事情。

我想要给大家展示的例子是存储:

make changes make some more changes make even more changes  try moc.save()

在 Core Data 中我们有一个方法是用来保存您所做的更改的,它的名字很简单:save。您或许会执行某些更改,当您结束更改之后,您就会通知上下文去保存您所做的更改。这个方法相当简单,但是我们还可以做得更好。

moc.performChanges {   make changes   make some more changes   make even more changes }

我们通知上下文我们希望做一些改变,然后我们就将我们所做的所有改变封装到了一个简单的闭包当中。这样做很容易理解,因为我们可以看到所有的更改都封装到了一个闭包里面。

这是一个非常好的模式,清晰易读,很难犯错,并且实现也是非常的简单:

extension NSManagedObjectContext {   public func performChanges(block: () -> ()) {     performBlock {       block()       self.saveOrRollback()     }   } }

我们添加了这个 performChanges 方法,它只是简单地调用这个闭包然后执行存储即可。如果您创建了类似的闭包封装的话,那么 UIApplication API 会变得更加易用。

NSNotification - 观察变化

Core Data 使用 NSManagedObjectContextObjectsDidChangeNotification 来观察变化,这让我们能够使用灵活的方式来构建代码。只要 Core Data 当中有对象发生了变化(无论是谁做出的更变操作,也无论它为什么这样做),NSNotificaiton 都会触发,这样我们就可以将我们的代码与之关联,保证 UI 能够实时更新。

通常情况下,我们会这样多次实现这个功能:

func observe() {   let center = NSNotificationCenter.defaultCenter()   center.addObserver(     self,     selector: "cityDidChange:",     name: NSManagedObjectContextObjectsDidChangeNotification,     object: city) }  @objc func cityDidChange(note: NSNotification) {   guard let city = note.object as? City else { return }   if city.deleted {     navigationController?.popViewControllerAnimated(true)   } else {     nameLabel.text = city.name   } }

我们使用了 NSNotificationCenter.defaultCenter() ,为其添加了一个观察者,设置 selector ,然后将通知名称传递进去(在本例当中,我们传递的是我们想要观察的 city 对象名称)。我们随后就使用 cityDidChange 方法,我们将对象从 notificaiton 当中提取出来,然后检查其是否是 City 类型。 我们基本完成了观察的操作,但是我们可以做到更好

observer = ManagedObjectObserver(object: city) { [unowned self] type in   switch type {   case .Delete:     self.navigationController?.popViewControllerAnimated(true)   case .Update:     self.nameLabel.text = city.name   } }

我们创建了一个 ManagedObjectObserver ,传递进去 City 对象,然后接着,当对象发生变更的时候,这个闭包都会运行。我们就会进行检查:“它是否被删除掉了?”。然后我们就弹出视图控制器。如果 city 发生了改变,我们就用新的 city.name 来更新 nameLabel 的文本值。这是非常易读、也是非常容易理解的。

我们该如何实现这个功能呢? 这里我就要偷个懒了,给大家展示一下图片

extension NSManagedObjectContext {   public func addObjectsDidChangeNotificationObserver(handler: ObjectsDidChangeNotification -> ())     -> NSObjectProtocol {       let nc = NSNotificationCenter.defaultCenter()       let name = NSManagedObjectContextObjectsDidChangeNotification       return nc.addObserverForName(name, object: self, queue: nil) {         handler(ObjectsDidChangeNotification(note: $0))       }   } }

我们通过添加这个辅助器来观察通知。我们获取默认的通知中心,然后添加通知名称从而添加实际的观察期。在最后一行,我们使用了一个封装,这让我们能够享受到类型安全的优点。这个封装是一个简单的 Swift 结构体,它其中唯一的一个属性就是它所封装的通知本身,它的名称就是 ObjectsDidChangeNotification

为了使用这个结构体,我们需要添加一个类型安全的属性。这个通知当中的 userInfo 字典包含有内容,我们将会用一种类型安全的方法将其提取出来。如果您需要获取这些对象的话,那么我们在这个辅助类结构体上还提供了类型安全的插入对象方法:

public struct ObjectsDidChangeNotification {   private let notification: NSNotification   init(note: NSNotification) {     assert(note.name == NSManagedObjectContextObjectsDidChangeNotification)     notification = note   }   public var insertedObjects: Set<ManagedObject> {     return objectsForKey(NSInsertedObjectsKey)   }   public var updatedObjects: Set<ManagedObject> {     return objectsForKey(NSUpdatedObjectsKey)   }   public var deletedObjects: Set<ManagedObject> {     return objectsForKey(NSDeletedObjectsKey)   }    [...] }

突然之间,我们的代码就更加 Swift 化,也更加易于使用了。这些都是这个辅助器当中的一些例子,我们通过创建它们来让代码变得更加 Swift 化。

我们使用了协议和协议扩展来让我们的代码更加易读。

我们同样还是用了其他的一些小技巧,但是其主要依据是:您可以在您的代码当中创建一些小的辅助类,从而让生活更加美好,同时也让别人能够更容易阅读您的代码。使用旧有的 API 是很不错的,因为这些使用方式已经得到了实战检验;这些代码已经存在了很多年……并且出现的问题都已经得到了修复。但是,我们可以让这些 API 更加好用( 并且让我们的生活更加美好 )。

当我们创建完这些辅助类之后,最重要的一点就是 保持原来设计理念 。我们要让别人能够轻松地阅读我们的代码,就算它们不知道这些封装里面是如何实现的,这都是无所谓的。

问:我看到一些人使用 Core Data 的时候是使用结构体而不是 NSManagedObject,它们使用结构体进行了封装,我自己也对其进行了实验,我很喜欢这种做法,不过很明显,我对 Core Data 的内部实现就不甚了解了,这是不是一个很糟糕的主意呢?感觉这些 Swift 化的方式可能会让我们失去 Core Data 原本的灵魂,您对此有什么看法呢?我们是否应该这样做呢?

Daniel:这是一个很好的问题,Chris。通常在 Core Data 当中您应该设置一些属性。在这个 City 类当中我们设置了一个 name 属性和一个 mayor 属性,然后将其放到 NSManagedObject 子类当中。很多人一直在尝试将这些数据放到 Swift 结构体当中,这样就可以更加 Swift 风格化。Core Data 需要大量使用管理对象 (managed object) 和持久化存储协调器 (Persistence store coordinator),从而才能为您带来性能方面的优势。一旦您将其转变为结构体,那么您就丧失了性能方面的提升。关于这一点我可以讲半个小时,但是它们之间的最重大区别就在于性能。如果您的对象数量不多的话,那么将其转变到结构体当中是一个挺好的选择,但是您必须要意识到:一旦您的应用很庞大的话,那么这个做法很可能是一个糟糕的主意,因为您的性能将会被严重拖缓。这就是我的一个简要的回答。

问:您能回到前面的幻灯片那里吗,也就是您说有既有 API 的地方。您在那里故意写错了 “existing” 这个单词 (“exiting),我想知道的是,您是否认为这个既有的 API 将会消失,被 Swift 当中更容易的 API 所取代吗?

Daniel:没错,这也是一个很好的问题。我当然不是这么认为的,这正是我所想提及的一点。Core Data 在很多年前就出现了,它在 Swift 世界当中是非常奇怪的。但是您需要记住,这类代码已经被大量使用了,不仅仅是在成千上万个 iOS 应用当中,在此之前,Mac 上很多应用都使用了 Core Data。苹果在它们的应用中也大量使用这个 API,并且十二年来它们还有一个团队专门负责修复这个 API 的 BUG。如果苹果说:“我们要做一个新的东西来替代 Core Data”,那么您在 2028 年就可以成功看到这一幕场景了,并且它会变得和 Core Data 一样牢固。我觉得替代 Core Data 并不现实,当然如果您需要在应用中存储内容的话,您需要评估一下 Core Data 是否满足您的需求。它同样还有其他的解决方案,您只需要将合适的砖头放到合适的位置上就可以了。如果 Core Data 能够满足大家的需求,那么我不会认为苹果会去写一个东西去替代它。它的功能已经比较完善了。

See the discussion on Hacker News .

原文  https://realm.io/cn/news/tryswift-daniel-eggert-modern-core-data/
正文到此结束
Loading...