保持你的模型在应用中同步,永不再有不一致。使用最新的 Swift 特性设计。
struct
和 Identifiable
对象CohesionKit 作为一个单一数据源解决方案,它处理你对象的生命周期和来自 *任何* 源的同步。
你应该将 CohesionKit 放在你的数据源 (REST API, GraphQL, ...) 前面,然后再将数据返回给你的应用。
sequenceDiagram
autonumber
YourApp ->>DataSource: findBooks
DataSource ->>GraphQL: query findBooks
GraphQL -->>DataSource: FindBooksQueryResult
DataSource ->>CohesionKit: store books [A,B,C]
CohesionKit -->> YourApp: Publisher<[A,B,C]>
WebSocket ->> WebSocketListener: book A updated
WebSocketListener ->> CohesionKit: update book A
CohesionKit -->> YourApp: Publisher<[A,B,C]>
dependencies: [
.package(url: "https://github.com/pjechris/CohesionKit.git", .upToNextMajor(from: "0.7.0"))
]
库带有一个 示例项目,以便你可以看到真实的使用案例。它主要展示了
首先创建一个 EntityStore
的实例
let entityStore = EntityStore()
EntityStore
允许你存储 Identifiable
对象
struct Book: Identifiable {
let id: String
let title: String
}
let book = Book(id: "ABCD", name: "My Book")
entityStore.store(book)
然后你可以在代码中的任何位置检索对象
// somewhere else in the code
entityStore.find(Book.self, id: "ABCD") // return Book(id: "ABCD", name: "My Book")
每次 EntityStore
中的数据更新时,都会向任何已注册的观察者触发通知。要将自己注册为观察者,只需使用 store
或 find
方法的结果
func findBooks() -> some Publisher<[Book], Error> {
// 1. load data using URLSession
URLSession(...)
// 2. store data inside our entityStore
.store(in: entityStore)
.sink { ... }
.store(in: &cancellables)
}
entityStore.find(Book.self, id: 1)?
.asPublisher
.sink { ... }
.store(in: &cancellables)
CohesionKit 具有 弱内存策略,你应该了解一下。因此,来自 entityStore.store 的返回值必须被强引用,以避免丢失值。
为了简洁起见,接下来的示例将省略
.sink { ... }.store(in:&cancellables)
。
要存储包含嵌套标识对象的对象,你需要使它们遵循一个协议:Aggregate
。
struct AuthorBooks: Aggregate {
var id: Author.ID { author.id }
var author: Author
var books: [Book]
// `nestedEntitiesKeyPaths` must list all Identifiable/Aggregate this object contain
var nestedEntitiesKeyPaths: [PartialIdentifiableKeyPath<Self>] {
[.init(\.author), .init(\.books)]
}
}
然后 CohesionKit 处理这三个实体的同步
只接受可写的键路径。 使用 KeyPath (let) 将导致错误:“键路径值类型 KeyPath 无法转换为上下文类型 WritableKeyPath”
这使你能够彼此独立地检索它们
let authorBooks = AuthorBooks(
author: Author(id: 1, name: "George R.R Martin"),
books: [
Book(id: "ACK", title: "A Clash of Kings"),
Book(id: "ADD", title: "A Dance with Dragons")
]
)
entityStore.store(authorBooks)
entityStore.find(Author.self, id: 1) // George R.R Martin
entityStore.find(Book.self, id: "ACK") // A Clash of Kings
entityStore.find(Book.self, id: "ADD") // A Dance with Dragons
你也可以随意修改它们中的任何一个。请注意,更改从对象本身和聚合对象中都可见
let newAuthor = Author(id: 1, name: "George R.R MartinI")
entityStore.store(newAuthor)
entityStore.find(Author.self, id: 1) // George R.R MartinI
entityStore.find(AuthorBooks.self, id: 1) // George R.R MartinI + [A Clash of Kings, A Dance with Dragons]
你可能会考虑直接在
Author
上存储书籍 (author.books
)。在这种情况下,Author
需要实现Aggregate
并将books
声明为嵌套实体。然而,我强烈建议你不要将
Identifiable
对象嵌套到其他Identifiable
对象中。 如果你想了解更多关于这个主题的信息,请阅读 处理关系 这篇文章。
目前我们只关注了 entityStore.store
,但 CohesionKit 还提供了另一种存储数据的方法:entityStore.update
。
有时两者都可以使用,但它们各有不同的用途
store
适用于存储从 Web 服务检索的完整数据,例如 GET /user
。update
通常用于部分数据。 它也是从 websockets 接收事件时的首选方法。从 0.13 版本开始,库支持枚举类型。 请注意,你需要遵循 EntityWrapper
协议,并为你想要存储的每个实体提供计算型的 getter/setter。
enum MediaType: EntityWrapper {
case book(Book)
case game(Game)
case tvShow(TvShow)
func wrappedEntitiesKeyPaths<Root>(relativeTo parent: WritableKeyPath<Root, Self>) -> [PartialIdentifiableKeyPath<Root>] {
[.init(parent.appending(\.book)), .init(parent.appending(\.game)), .init(parent.appending(\.tvShow))]
}
var book: Book? {
get { ... }
set { ... }
}
var game: Game? {
get { ... }
set { ... }
}
var tvShow: TvShow? {
get { ... }
set { ... }
}
}
struct AuthorMedia: Aggregate {
var author: Author
var media: MediaType
var nestedEntitiesKeyPaths: [PartialIdentifiableKeyPath<Self>] {
[.init(\.author), .init(wrapper: \.media)]
}
}
有时你需要检索数据而不知道对象 ID。 常见的情况是当前用户。
CohesionKit 提供了一种合适的机制:别名。 别名允许你使用键注册和查找实体。
extension AliasKey where T == User {
static let currentUser = AliasKey("user")
}
entityStore.store(currentUser, named: .currentUser)
然后在其他地方请求它
entityStore.find(named: .currentUser) // return the current user
与常规实体相比,别名对象是长生命周期的对象:即使没有人观察它们,它们也会保留在存储中。 这允许在别名值更改时通知已注册的观察者
entityStore.removeAlias(named: .currentUser) // observers will be notified currentUser is nil.
entityStore.store(newCurrentUser, named: .currentUser) // observers will be notified that currentUser changed even if currentUser was nil before
存储数据时,CohesionKit 实际上要求你设置修改时间戳。 Stamp
用作比较数据新鲜度的标记:时间戳越高,数据越新。
默认情况下,CohesionKit 将使用当前日期作为时间戳。
entityStore.store(book) // use default stamp: current date
entityStore.store(book, modifiedAt: Date().stamp) // explicitly use Date time stamp
entityStore.store(book, modifiedAt: 9000) // any Double value is valid
如果由于某种原因你尝试存储时间戳低于已存储数据的日期的数据,则更新将被丢弃。
CohesionKit 具有弱内存策略:只要有人使用对象,对象就会保存在 EntityStore
中。
为此,只要你对数据感兴趣,就需要保留观察者
let book = Book(id: "ACK", title: "A Clash of Kings")
let cancellable = entityStore.store(book) // observer is retained: data is retained
entityStore.find(Book.self, id: "ACK") // return "A Clash of Kings"
如果你不创建/保留观察者,那么一旦实体不再有观察者,它们将自动从存储中丢弃。
let book = Book(id: "ACK", title: "A Clash of Kings")
_ = entityStore.store(book) // observer is not retained and no one else observe this book: data is released
entityStore.find(Book.self, id: "ACK") // return nil
let book = Book(id: "ACK", title: "A Clash of Kings")
var cancellable = entityStore.store(book).asPublisher.sink { ... }
let cancellable2 = entityStore.find(Book.self, id: "ACK") // return a publisher
cancellable = nil
entityStore.find(Book.self, id: "ACK") // return "A Clash of Kings" because cancellable2 still observe this book
本项目根据 MIT 许可证发布。 有关详细信息,请参阅 LICENSE 文件。