SimpleCoreData实践


面向协议编程是Swift语言的一个很大的特点,wwdc中有一节经典的Session面向协议编程对Swift这个语法特性做了详细的分析。下面分享的内容主要是利用Swift面向协议编程特性,封装CoreData数据库的API。至于为何要选择CoreData数据库也是因为Coredata很多API其实对于初学者非常不友好,也是想通过Swift语言的一些优秀特性来简化API的操作,也特意为此起了一个名字叫SimpleCoredata。

CoreData的基本思想

CoreData核心思想就是,操控数据库时,避免写繁琐的sql语句,而用更友好的对象操控的方式来使用数据库。CoreData如果你不太了解的话,建议看下苹果的官方文档。这篇博客并不会对CoreData进行详细的讲解,我主要是想分享下如何利用Swift语言的一些特性设计合理的API。

CoreData虽然说可以简化写代码的量,但是也有很多负面问题,比如coredata对象操控造成没办法很好的指定主键,保证数据唯一性时要做一些过多的操作。还有对象模型合并时,需要写大量的合并代码,以及读写性能的问题等等。其实业界对CoreData的吐槽也很多,对于一些大型的项目确实Coredata还是有一些坑存在。

这几年苹果的开发者大会经常有CoreData的相关Session,也在不停的优化和改善CoreData的体验。如果想使用CoreData数据库作为项目开发,建议最好观看下Core Data Best Practices这个session,里面的讲解对CoreData优化方面都有详细的解释。之后你看完这篇文章就会发现,其实用CoreData操作数据库存储,可以如此简单,只要几行代码即可,所以你想要做一些小项目使用到数据库时,CoreData还是蛮合适的。

好了暂时对CoreData的解释就这么多了,下面开始分享下Swift面向协议编程的思想了。

Swift的面向协议编程

首先解释下为什么苹果要提出面向协议编程,其实跟OOP遇到的问题有很大的关系,由于现在很多项目越来越复杂,设计类的继承结构非常的深,造成开发者阅读起来比较困难,并且还经常会出现修改了一个子类的方法,莫名其妙的影响到了其他类的实现。

在OOP中为了解决此问题使用了很多设计模式。其实设计模式大多是利用组合、代理、装饰来减弱继承过多的问题。设计模式本质上是迫不得已才引入的,虽然很有效但是大家必须要遵守设计模式的规则去实现代码。可是如果大家不遵守这个规则,语言层面也不会报错,就会造成之后代码的可维护性越来越差。为了语言层面上解决这些问题,就出现面向协议编程(POP)。Swift语言也就是顺应了这个潮流。

extension用法

下面我结合代码来说下Swift的protocol中extension用法是如何避免继承的。先来看下OOP编程中为了重用一个类的方法,往往用如下的写法:

class ParentClass {
    func learnSwift() {
        print("learn swift")
    }
}

class childClass:ParentClass {
    func advanceCourse() {
        learnSwift()
    }
}

使用swift语言protocol协议的extension语法特性,可以扩展一个方法的实现就可以这样写。

protocol ParentProtocol {
    func learnSwift()
}

extension ParentProtocol {
    func learnSwift() {
        print("learn swift")
    }
}

class childClass:ParentProtocol {
    func advanceCourse() {
        learnSwift()
    }
}

可以看出来,代码量有一定增加,但是把learnSwift作为一个公共的方法定义到接口中,明显比这个方法隐藏在父类中,增加了可读性。下面再来看一段代码。

protocol FatherProtocol {
    func weight() -> CGFloat
}

extension FatherProtocol {
    func weight() -> CGFloat {
        return 60
    }
}

protocol MatherProtocol {
    func height() -> CGFloat
}

extension MatherProtocol {
    func height() -> CGFloat {
        return 175
    }
}

class PersonClass:FatherProtocol,MatherProtocol {
    func BMI() {
        print("BMI index height \(height()), weight \(weight())")
    }
}

从上面的使用可以看出,协议和扩展功能可以解决横向多态问题。如果是OOP编程的话,想把一个父类拆分,往往的做法是父类再增加一个父类,造成继承越来越深。现在利用Swift语言的特性就可以用组合的方式提炼出来公共方法,然后进行横向扩展,代码可读性会大大增加。

associatedtype的使用

相对于OOP类对象,接口中往往缺少实例变量的概念。所以类中的实例变量如果需要重用的话,在protocol中应该如何设计哪?这就要利用Swift语法protocol的另一个特性associatedtype。这个就相当于给协议定义了一个公共的实例变量。下面看个例子

protocol Container {
    associatedtype Item: Equatable
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}

这个协议是所有集合类都要实现的,因为集合类都要存储实例变量,所以associatedtype Item就定义了公共的实例变量。然后所有实现的类用模板语法来定义就可以达到重用的目的,例如Stack的定义。

struct Stack<Element>: Container {
    var items = [Element]()
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }

    mutating func append(_ item: Element) {
        self.push(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> Element {
        return items[i]
    }
}

其实associatedtype的很多概念设计到了Swift模板语法的特性,如果要深入了解可以看下swift Generics语法官方文档。

SimpleCoreData实践

上面讲了这么多铺垫,下面说下SimpleCoreData框架的实践,首先看下这个框架的UML类图结构。 UML1

从类图里面可以看出来,CoreDataStroageNSManagedObjectContextNSManagedObjectCoreDataObservable这几个类是协议的真正实现,将来如果替换成其他类型的数据库,上层protocol的设计可以不用改变,方便了底层的替换。其实这在OOP中是运用了设计模式的一个重要原则依赖接口,不依赖实现(IOC)。

这里就简单说下Objective-c语言中实现IOC的方法,往往需要一个容器来记录哪些接口被实现了,因此要定义一个IOCContainer的共有类,然后通过registorComplement方法把所有实现对应的Protocol接口,注册到IOCContainer中。然后上层代码的调用都使用Protocol的方法,这样就实现了接口依赖。Objective-c为何用这么麻烦的方法实现,显然是因为语法上不支持这种特性,并且这样实现容易出现的问题是,假如有一些protocol的实现没有注册到Container中时,这个问题不容易被发现,一旦上层调用就容易崩溃。

下面看下Swift如何实现这个设计模式的,这里可能要用到Swift一个新的语法OpaqueTypes。这个是通用类型定义,具体我们看下面的代码。


public protocol Storage:CustomStringConvertible,Equatable {
    func storePath() -> URL
    var context: Context! { get }
}

public class CoreDataStorage: Storage {
    public var storeFileName: String
    private var mainContext: Context!
    required public init(objectModelName:String,fileName:String,bundle:Bundle? = Bundle.main) {
        // todo
    }
    public var context: Context!{
        return mainContext
    }
    public func storePath() -> URL {
        return URL(fileURLWithPath: documentsDirectory()).appendingPathComponent(storeFileName)
    }
}

static public func openDB(objectModelName: String, dbName: String) -> some Storage {
    if let db = DBFactory.manager.containers[dbName] {
        return db
    }
    let result = synchronized(lock, { () -> CoreDataStorage in
        let db = CoreDataStorage(objectModelName: objectModelName, fileName: dbName)
        DBFactory.manager.containers[dbName] = db
        return db
    })
    return result;
}

some Storage定义了实现Storage协议这一类型的返回对象,通过这个语法就可以把所有的实现都封装起来,只暴露接口给上层。然后上层的调用只要简单的一行代码就可以搞定。

let database:some Storage = DBFactory.openDB(objectModelName: "SimpleDataBase", dbName: "TestCoreData")

这样底层数据库的实现替换了也不会影响上层的代码。注意我上面的代码是简化了框架中的实现,具体的实现要看下源码,会复杂一些但是思想是一样的。

Entity实践

针对之前提到的Swift协议中extension用法,这里看下SimpleCoreData中时如何应用的。

public protocol Entity:Equatable {
    var primeKey:String {get}
    func syncDictionary(_ jsonObject: [String:Any])
}

public func == <T:Entity>(lhs: T, rhs: T) -> Bool {
    return lhs.primeKey == rhs.primeKey
}

数据库存储的对象,很重要的一个属性就是primeKey。往往在比较两个对象是否一样的时候,只要primeKey一致就可以了。所以Entity协议就把比较操作抽离出来作为一个公共方法。而syncDictionary这个方法是把数据存储到数据库中常用的手段,这里就需要实现的类完成这个操作了。

DBObservable实践

数据库存储中,上层经常会用到一个方法,就是当存储的数据变化时,通知上层做一些UI方面的刷新。下面就来看下如何利用associatedtype把数据库这个公共操作抽离出来。


public protocol DBObservable {
    associatedtype Elment:Entity
    init(context:Context)
    func observer(_ closure:@escaping ([StorageDataChange<Elment>]) -> Void) -> Void
}

public enum StorageDataChange<T:Entity> {
    case update(T)
    case delete(T)
    case insert(T)
    case fetch(T)
    public func object() -> T {
        switch self {
        case .update(let object): return object
        case .delete(let object): return object
        case .insert(let object): return object
        case .fetch(let object): return object
        }
    }
    public var isDeletion: Bool {
        if case .delete = self {
            return true
        }
        return false
    }
    public var isUpdate: Bool {
        if case .update = self {
            return true
        }
        return false
    }
    public var isInsertion: Bool {
        if case .insert = self {
            return true
        }
        return false
    }
    public var isFetch: Bool {
        if case .fetch = self {
            return true
        }
        return false
    }
}

从上面的代码中可以看出数据库观察者有重要的两个属性,首先要知道要观察的实体对象是什么,这里就用到了通用的associatedtype方法,其次就要知道数据库当前的context,也就是数据的内存中分布的情况。然后可以定义数据库更新常用操作的枚举(update,delete,insert,fetch),就可以方便的抽离出来公共方法,然后实现的类,只要关注func observer(_ closure:@escaping ([StorageDataChange<Elment>]) -> Void) -> Void方法实现就可以了。

swift package管理

SimpleCoreData 目前是使用Swift Package来管理的。相对于pod中心化的仓库管理,Swift Package是去中心化的,更像Carthage的用法。如果想了解详细的用法可以参考苹果的文档swift package。苹果还提供了xcode工程如何快速集成swift package的方法

总结

SimpleCoreData 完整的实现,已经放在了GitHub上链接地址。这个是自己对Swift语言面向协议编程的一个实践,还有很多不完善的地方,抛砖引玉,希望大家多多给些意见。

如果你喜欢这篇文章,谢谢你的赞赏

图3

如有疑问请联系我