面向协议编程是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类图结构。
从类图里面可以看出来,CoreDataStroage
、NSManagedObjectContext
、NSManagedObject
、CoreDataObservable
这几个类是协议的真正实现,将来如果替换成其他类型的数据库,上层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语言面向协议编程的一个实践,还有很多不完善的地方,抛砖引玉,希望大家多多给些意见。