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

2020-04-26  本文已影响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")!
}

其中:

其次,定义一个表示老版本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之后,

上一篇下一篇

猜你喜欢

热点阅读