6

SimpleCoreData实践

 4 years ago
source link: https://mengtnt.com/2020/09/14/coredata-swift.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

面向协议编程是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语言面向协议编程的一个实践,还有很多不完善的地方,抛砖引玉,希望大家多多给些意见。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK