美文网首页
如何让model兼容多个版本的API

如何让model兼容多个版本的API

作者: 醉看红尘这场梦 | 来源:发表于2020-04-26 11:25 被阅读0次

    在前面的内容中,我们都在讨论JSON和model转换时用到的各种语言层面的内容。这一节,我们来看一些更偏业务层面的场景。

    有时,我们接手的任务并不是从零开始的。例如,我们有两个版本返回视频信息的API,老版本中视频创建日期的格式是这样的:

    {
      "created_at": "Oct-24-2017"
    }
    
    

    而新版本API中日期的格式是这样的:

    {
      "created_at" : "2017-08-28T00:24:10+0800"
    }
    
    

    现在,如何让我们的model在保证兼容性的前提下,过度到新的API呢?为了更好的演示这一节的内容,我们把之前使用的Episode对象进行了一些简化,让它只保留一个表示时间的字段:

    struct Episode: Codable {
        var createdAt: Date
    
        enum CodingKeys: String, CodingKey {
            case createdAt = "created_at"
        }
    }
    
    

    现在,为了兼容老版本的API,我们可以这样。

    首先,定义一个包含版本信息的结构EpisodeCodingOptions

    struct EpisodeCodingOptions {
        enum Version {
            case v1
            case v2
        }
    
        let apiVersion: Version
        let dateFormatter: DateFormatter
    
        static let infoKey = CodingUserInfoKey(
            rawValue: "io.boxue.episode-coding-options")!
    }
    
    

    其中:

    • apiVersion用于区分API版本;
    • dateFormatter用于表示不同版本的日期格式;
    • infoKey是一个CodingUserInfoKey对象,它的作用,有点儿类似Dictionary中的key。稍后我们就会看到,每个encoder都可以通过这个类型的对象包含一些关于编码的额外信息,它的参数只用于标识不同的key,没有其他的含义。由于所有的EpisodeCodingOptions都应该使用相同的标识符,因此,我们把它定义成class attribute就可以了;

    其次,定义一个表示老版本API的EpisodeCodingOptions对象:

    let formatter = DateFormatter()
    formatter.dateFormat = "MMM-dd-yyyy"
    
    let options = EpisodeCodingOptions(
        apiVersion: .v1, dateFormatter: formatter)
    
    

    第三,为了适配老版本的API,我们修改一下在上一节中实现的全局encode函数。先给它添加一个参数,用于传递版本信息:

    func encode<T>(of model: T,
        options: [CodingUserInfoKey: Any]!) throws where T: Codable {
        // ...
    }
    
    

    并且,当options不为nil的时候,我们用它设置encoder

    func encode<T>(of model: T,
        options: [CodingUserInfoKey: Any]!) throws where T: Codable {
        // ...
        if options != nil {
            encoder.userInfo = options
        }
        // ...
    }
    
    

    第四,我们就可以在编码的时候得到要使用的版本信息了。为了使用这个信息,我们得自定义encode方法:

    extension Episode {
        func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)
    
            if let options =
                encoder.userInfo[EpisodeCodingOptions.infoKey] as?
                    EpisodeCodingOptions {
                let date = options.dateFormatter.string(from: createdAt)
                try! container.encode(date, forKey: .createdAt)
            }
            else {
                fatalError("Can not read coding options.")
            }
        }
    }
    
    

    在上面的代码里,我们先读取了encoder.userInfo[EpisodeCodingOptions.infoKey]并尝试把它转型成一个EpisodeCodingOptions。如果转型成功了,就表示我们得到版本信息了。这里,我们直接读取options.dateFormatter生成对应的字符串,并把这个字符串编码到createdAt对应的值就好了。当然,这里,我们也可以通过读取options.apiVersion做一些针对API版本的特别动作。

    这样,所有的工作就都完成了,重新执行一下,就会看到下面这样的结果:

    {
      "created_at" : "Aug-28-2017"
    }
    
    

    当我们要编码到新API时,只要定义.v2版本的EpisodeCodingOptions就好了:

    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
    
    let options = EpisodeCodingOptions(
        apiVersion: .v2, dateFormatter: formatter)
    
    

    重新执行下,就会得到下面的结果:

    {
      "created_at" : "2017-08-28T00:24:10+0800"
    }
    
    

    理解了encode的方法之后,大家可以试着自己写一下用于解码的init方法,原理是完全一样的,我们就不重复了。

    处理Key的个数不确定的JSON

    解码Key不确定的JSON

    除了兼容性之外,另外一类我们还没提过的场景,就是JSON中key的个数是不确定的,例如,用视频id作为key:

    let response = """
    {
        "1":{
            "title": "Episode 1"
        },
        "2": {
            "title": "Episode 2"
        },
        "3": {
            "title": "Episode 3"
        }
    }
    """
    
    

    面对这种情况,显然我们无法把所有的id值都通过model属性一一对应起来。怎么办呢?

    首先,我们为这个新的JSON格式定义一个model:

    struct Episodes: Codable {
    
    }
    
    

    其次,把之前表示视频的Episode修改成下面这个样子,让它只包含表示视频idtitle的属性:

    struct Episodes {
        /// ...
        struct Episode: Codable {
            let id: Int
            let title: String
        }
    }
    
    

    第三,在Episodes里,我们要定义一个更灵活的CodingKey类型来表示JSON和model的对应关系:

    struct Episodes {
        struct EpisodeInfo: CodingKey {
            var stringValue: String
            init?(stringValue: String) {
                self.stringValue = stringValue
            }
    
            var intValue: Int? { return nil }
            init?(intValue: Int) { return nil }
    
            static let title = EpisodeInfo(stringValue: "title")!
        }
    }
    
    

    这里,对于一个遵从了CodingKey的类型来说,stringValueintValue属性,以及接受StringInt为参数的init方法是protocol强制要求的,我们必须定义它们。稍后就会看到,由于我们可以从JSON的key中读到id,因此在EpisodeInfo里,我们只要在最后,定义title在model中的映射规则就好了。

    最后,我们用一个Array<Episode>存储JSON中的所有内容:

    struct Episodes {
        /// ...
        var episodes: [Episode] = []
    }
    
    

    这样,model的部分就完成了,我们最终还是用了一个Array,解决了JSON key个数不确定的问题。接下来,为了从JSON自动生成model,我们只要重写Episodesinit方法就好了:

    init(from decoder: Decoder) throws {
        let container = try decoder.container(
            keyedBy: EpisodeInfo.self)
    
        var v = [Episode]()
        for key in container.allKeys {
            let innerContainer = container.nestedContainer(
                    keyedBy: EpisodeInfo.self, forKey: key)
    
            let title = try innerContainer.decode(
                String.self, forKey: .title)
            let episode = Episode(id: Int(key.stringValue)!,
                title: title)
    
            v.append(episode)
        }
    
        self.episodes = v
    }
    
    

    在上面的代码里:

    首先,用EpisodeInfo定义的规格创建了解码用的容器;

    其次,用一个for循环,遍历了JSON中的所有key。在这个循环内部,我们先用EpisodeInfo读到了和没一个key对应的子容器,在这个子容器中,通过解码title得到了对应的值,并且,通过Int(key.stringValue)!得到了对应的视频id。这样,创建Episode需要的所有值就都准备好了;

    第三,我们创建Episode对象,并把它添加到保存结果的临时数组里:

    最后,用临时变量更新self.episodes的值。之所以这样做,是为了避免JSON中存在非法数据而破坏之前的历史数据,我们只有在所有值都转换成功之后,才更新原始值。

    这样,所有的代码就完成了。我们定义一个decode全局函数来观察下解码的结果:

    func decode<T>(response: String,
        of: T.Type) throws where T: Codable {
        let data = response.data(using: .utf8)!
        let decoder = JSONDecoder()
        let model = try decoder.decode(T.self, from: data)
    
        dump(model)
    }
    
    

    和之前的全局encode类似,我们只是封装了之前的解码代码。现在我们试着解码一下之前的response

    decode(response: response, of: Episodes.self)
    
    

    执行一下,就能看到下面这样的结果了:

    ▿ 3 elements
      ▿ Codable.Episodes.Episode
        - id: 2
        - title: "Episode 2"
      ▿ Codable.Episodes.Episode
        - id: 1
        - title: "Episode 1"
      ▿ Codable.Episodes.Episode
        - id: 3
        - title: "Episode 3"
    
    

    可以看到,这和我们在Episodes中设计的数据结构是一样的。

    编码Key不确定的JSON

    了解了解码之后,我们再来看如何编码Episodes对象,基本思路其实是一样的,直接来看encode的源代码:

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(
            keyedBy: EpisodeInfo.self)
    
        for episode in episodes {
            let id = EpisodeInfo(
                stringValue: String(episode.id))!
            var nested = container.nestedContainer(
                keyedBy: EpisodeInfo.self, forKey: id)
    
            try nested.encode(episode.title, forKey: .title)
        }
    }
    
    

    首先,我们还是用EpisodeInfo约定的规格创建了用于编码的容器;

    其次,我们遍历了episodes数组,用id创建了JSON中的每一个key,并用这个key创建了子容器;

    最后,在这个子容器里,我们编码进了title的值;

    这样,编码过程就完成了。为了方便利用之前的解码结果,我们把刚才实现的全局decode改一下:

    func decode<T>(response: String,
        of: T.Type) throws -> T where T: Codable {
        let data = response.data(using: .utf8)!
        let decoder = JSONDecoder()
        let model = try decoder.decode(T.self, from: data)
    
        return model
    }
    
    

    然后,把之前解码后的结果再编码回来:

    try encode(
        of: decode(response: response, of: Episodes.self),
        options: nil)
    
    

    执行一下,可以看到下面这样的结果了:

    {
      "2" : {
        "title" : "Episode 2"
      },
      "1" : {
        "title" : "Episode 1"
      },
      "3" : {
        "title" : "Episode 3"
      }
    }
    
    

    和最初,我们在response中的内容是一样的。

    以上,就是这一节的内容,在了解了如何处理版本过渡,以及Key不确定的JSON之后,

    相关文章

      网友评论

          本文标题:如何让model兼容多个版本的API

          本文链接:https://www.haomeiwen.com/subject/jenuehtx.html