命令解析该如何封装——Command

2,738 阅读3分钟

命令模式的实际使用场景

场景

对于直播业务,需要长连接 socket 实时更新直播间的状态,可是一个直播间内描述状态的 data 很多,如果一个细微的变化就要服务器把 all state 推到客户端,不但会浪费流量,客户端还需要解析数据,做状态diff更新ui。
如果服务器把数据库操作(增删改查 sql 指令)以 json string (server 指令历史片段) 的格式返回到客户端,客户端根据返回数据解析服务器数据应用到当前环境中。

未命名绘图.jpg

本文要做的就是把这些增删改查的 sql 指令 json 解析出来,不断的修改本地的 StateData,达到客户端&服务器同步

方案

面对多种 sql 语句,如何做到正确解析对应的 json?总不能一个个字符对吧

分析

  1. 归类总共有多少种 sql 类型
  2. 跟服务器定义好格式 json 格式,每条 sql 指令,[指令类型][序列号][propertyName][修改属性名]
  3. 使用 Command 模式,[指令类型] 对应具体类,[propertyName][修改属性名] 作为当前 Command 实例的上下文
  4. [序列号] 是服务器执行的历史顺序,这个一定要跟服务器一直,如果发现有 [序列号] 丢失,马上全量个性状态

接口结构

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)
    }
  }
}