命令模式的实际使用场景
场景
对于直播业务,需要长连接 socket 实时更新直播间的状态,可是一个直播间内描述状态的 data 很多,如果一个细微的变化就要服务器把 all state 推到客户端,不但会浪费流量,客户端还需要解析数据,做状态diff更新ui。
如果服务器把数据库操作(增删改查 sql 指令)以 json string (server 指令历史片段) 的格式返回到客户端,客户端根据返回数据解析服务器数据应用到当前环境中。
本文要做的就是把这些增删改查的 sql 指令 json 解析出来,不断的修改本地的 StateData,达到客户端&服务器同步
方案
面对多种 sql 语句,如何做到正确解析对应的 json?总不能一个个字符对吧
分析
- 归类总共有多少种 sql 类型
- 跟服务器定义好格式 json 格式,每条 sql 指令,
[指令类型][序列号][propertyName][修改属性名] - 使用 Command 模式,
[指令类型]对应具体类,[propertyName][修改属性名]作为当前 Command 实例的上下文 [序列号]是服务器执行的历史顺序,这个一定要跟服务器一直,如果发现有[序列号]丢失,马上全量个性状态
接口结构
Protocol Command 接口层,擦除Command 差异,得到结构
execute 方法的 input是 currentState,是 dict 类型
context 类型也是 dict 包含需要修改的 [propertyName][修改属性名]
protocol StateSyncCommand {
var context: [Any] { get }// 当前指令上下文
/*
receiver: 是传入数据源,当前 CurrentState
return: 是传出的数据源,执行完当前 sql 指令后的 CurrentState
*/
func execute(receiver: [String: Any]) throws -> [String: Any]
}
// 提供默认空实现
extension StateSyncCommand {
func execute(receiver: [String: Any]) throws -> [String: Any] {[:]}
}
具体每个 Command 实现
enum StateSyncCommandType: Int, Codable {
case AAA = 1, BBB, CCC
}
struct NullCommand: StateSyncCommand {
private(set) var context: [Any] = []
}
struct AAACommand: StateSyncCommand {
private(set) var context: [Any]
func execute(receiver: [String: Any]) throws -> [String: Any] {
var res = receiver
// 解析 context 应用到 receiver 上
return res
}
}
struct BBBCommand: StateSyncCommand {
private(set) var context: [Any]
func execute(receiver: [String: Any]) throws -> [String: Any] {
var res = receiver
// 解析 context 应用到 receiver 上
return res
}
}
struct CCCCommand: StateSyncCommand {
private(set) var context: [Any]
func execute(receiver: [String: Any]) throws -> [String: Any] {
var res = receiver
// 解析 context 应用到 receiver 上
return res
}
}
如何使用 Command
对于 currentState : RoomState 实例,在传入到 StateSyncEngine 的时候是做了一次 model -> dict 的转换,这样每一条 sql context 中要改变的属性 key 跟 model dict 中的属性可以一致。 执行完当前 [command] 数组后以后再 dict -> model 转换,回调给外面
不过 swift 提供了 @dynamicMemberLookup,把 RoomState 用它声明,可以这样通过它来做,这样减少了 model -> dict -> model 的过程
typealias SyncCommandInfo = [Any]
// 将 json 解析成 [command]
extension SyncCommandInfo {
func command() -> StateSyncCommand {
guard let t = first as? Int,
let type = StateSyncCommandType(rawValue: t) else {
Log.shared.error(tag: logTagStateSync, message: "数据解析出错,\(self)")
return NullCommand(context: self)
}
switch type {
case .AAA: return AAACommand(context: self)
case .BBB: return BBBCommand(context: self)
case .CCC: return CCCCommand(context: self)
}
}
}
public final class StateSyncEngine<T: Codable> {
// 命令执行完更新
public var updateModel: ((T)->())?
// 完全解析后的 model
public private(set) var model: T?
// 异步 json 解析 queue
private let serializeQueue = DispatchQueue(label: "StateSyncEngine.serialize.serial.queue")
// 需要解析的原始 json
private(set) var modelDict: [String: Any]?
private let modelType: T.Type
public init(type: T.Type) {
modelType = type
}
// 全量更新
public func update(all modeJson: String) {
if let data = modeJson.data(using: .utf8) {
serializeQueue.async {
do {
self.modelDict = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String:Any]
self.updateModel_()
} catch let e {
printLog(e)
}
}
}
}
// 增量更新
public func update(incrementData: [String: Any?]) {
guard var modelDict = modelDict else { return }
guard let states = incrementData["status"] as? [[String: Any]] else { return }
serializeQueue.async {
do {
try states.forEach { (state) in
guard let commandInfos = state["command"] as? [SyncCommandInfo] else { return }
try modelDict = commandInfos
.map { $0.command() }
.reduce(modelDict, {
try $1.execute(receiver: $0)
})
}
self.modelDict = modelDict
self.updateModel_()
} catch let e {
DispatchQueue.main.async {
self.requestAll?()
}
guard let e = e as? StateSyncError else { return }
Log.shared.error(tag: logTagRoomStateSync, message: e.info)
}
}
}
// json -> model
private func updateModel_() {
do {
guard let dict = modelDict else { return }
let data = try JSONSerialization.data(withJSONObject: dict, options: [])
model = try JSONDecoder().decode(modelType, from: data)
if let m = model {
DispatchQueue.main.async {
self.updateModel?(m)
}
}
} catch let e {
printLog(e)
}
}
}